```python
# Just importing stuff and setting-up matplolib
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import loadmat
import matplotlib as mpl
from scipy.optimize import minimize
mpl.rcParams["xtick.direction"] = "in"
mpl.rcParams["ytick.direction"] = "in"
mpl.rcParams["lines.markeredgecolor"] = "k"
mpl.rcParams["lines.markeredgewidth"] = 1.5
mpl.rcParams["figure.dpi"] = 200
from matplotlib import rc
rc('font', family='serif')
rc('text', usetex=True)
rc('xtick', labelsize='medium')
rc('ytick', labelsize='medium')
rc("axes", labelsize = "large")
def cm2inch(value):
    return value/2.54
def cm2inch(value):
    return value/2.54
def gauss_function(x, a, x0, sigma):
    return a*np.exp(-(x-x0)**2/(2*sigma**2))
def pdf(data, bins = 10, density = True):

    pdf, bins_edge = np.histogram(data, bins = bins, density = density)
    bins_center = (bins_edge[0:-1] + bins_edge[1:]) / 2

    return pdf, bins_center
```

# Experimental force measurement

In this notebook, we are gonna compute the force felt by our particle, experimentaly using the data of the preprint.

In the first part we will measure the force using the experimentaly measured potential.

On the second part we will use the local drifts and check if everything match.

```python
# Define logarythmic histrogram in order to have more points near the surface.
def logarithmic_hist(data,begin,stop,num = 50,base = 2):

    if begin == 0:
        beg = stop/num
        bins = np.logspace(np.log(beg)/np.log(base), np.log(stop)/np.log(base), num-1, base=base)
        widths = (bins[1:] - bins[:-1])
        bins = np.cumsum(widths[::-1])
        bins = np.concatenate(([0],bins))
        widths = (bins[1:] - bins[:-1])

    else:
        bins = np.logspace(np.log(begin)/np.log(base), np.log(stop)/np.log(base), num, base=base)
        widths = (bins[1:] - bins[:-1])

    hist,bins = np.histogram(data, bins=bins,density=True)
    # normalize by bin width
    bins_center = (bins[1:] + bins[:-1])/2

    return bins_center,widths, hist
```

Data loading

```python

raw_data = loadmat("fit_result_dur_27052020_n_r_fix_0p0513_wav_532_r_1p516_n_1.597.mat")["data"][:,0:3]
z_exp = raw_data[:,2] * 0.0513 * 1e-6
z_exp = z_exp - np.min(z_exp)
```

```python
# We compute the PDF
z_hist, _, P_z= logarithmic_hist(z_exp, 39e-9, 2e-6, num=50)
P_z = P_z / np.trapz(P_z, z_hist)
```

```python
# Just a plot of the PDF
plt.semilogy(z_hist, P_z, "o")
plt.xlabel("$z$ (m)")
plt.ylabel("$P_{eq}(z)$")
```

To compute de force we then avec to take the derivative of $\mathrm{log}(P_{eq})$

```python
F_eq =  np.gradient(np.log(P_z), z_hist) * 4e-21
#F_eq = (P_z[1:] - P_z[:-1]) / (z_hist[1:] - z_hist[:-1])
```

We want now to compare with the results of the force that can be computed from the result of the multifit, which gives us a good measurement of the following parameters.

B = 4.5
ld = 21 nm
lb = 532 nm

```python
B = 4.8
ld = 20.8e-9
lb = 532e-9

```

```python
z_F_th = np.linspace(20e-9,3e-6,10000)
# just a function to compute the theoritical force from the results of the multifit
f_th = lambda z:4e-21 * ( B/(ld) * np.exp(- z/(ld)) - 1/(lb))

F_th = f_th(z_F_th)

```

```python
plt.semilogx(z_hist, F_eq * 1e15, "s", label = "Force from E")
plt.plot(z_F_th, F_th * 1e15, color = "k", label = "multi-fit force")
plt.xlabel("$z$ (m)")
plt.ylabel("$F_{eq}$ (fN)")
plt.ylim((None,350))
plt.legend()
plt.show()
```

We see that there is an offset to take into account on $z$, to correctly take in into account we need to measure it correctly. in order to do it, i'll directly do a function that compute the $\chi ^2$ as a function of the offset that we apply on the trajectory.

As a reminder we do have this offset due to the way the holographic method we use. This method method mesure the distance between the imaging plane of the objective and the particle but not between the surface and the particle that we have to deduce form the data.

```python
def chi_2_force(offset):
    """ Function that permits to directly measure the chi 2 as a function as the offset on the trajectory"""
    offset = offset * 1e-9
    z = raw_data[:,2] * 0.0513 * 1e-6
    z = z-np.min(z)
    z = z-offset
    z_hist, _, P_z= logarithmic_hist(z, 39e-9, 2e-6, num=20)
    F_eq =  np.gradient(np.log(P_z), z_hist) * 4e-21
    #return z_hist, P_z
    return np.mean((F_eq*1e15 - f_th(z_hist)*1e15) ** 2)

```

```python
z_chi2 = np.linspace(1, 200, 30)
chi_2 = [chi_2_force(i) for i in z_chi2]
```

```python
plt.plot(z_chi2, chi_2 / max(chi_2), "o")
plt.xlabel("$z$ (nm)")
plt.ylabel("$\chi ^2 / max(\chi^2) $")
```

Okay ! we see that we have a minimum around $62.5$ nanometers, let's have a better measurement with the minimize algorithm !

```python
minimize(chi_2_force, x0 = 62.5,  method='Nelder-Mead', tol=1e-40)
```

     final_simplex: (array([[61.95602417],
           [61.95602417]]), array([25.38935383, 25.38935383]))
               fun: 25.389353833953738
           message: 'Optimization terminated successfully.'
              nfev: 143
               nit: 52
            status: 0
           success: True
                 x: array([61.95602417])

we see that the offset sould be equal to $62 ~ \mathrm{nm}$. Giving the following force :

```python
offset = 62 * 1e-9
z = raw_data[:,2] * 0.0513 * 1e-6
z = z-np.min(z)
z = z-offset
z_hist, _, P_z= logarithmic_hist(z, 39e-9, 2e-6, num=50)
F_eq =  np.gradient(np.log(P_z), z_hist) * 4e-21
```

```python
plt.semilogx(z_hist, F_eq * 1e15, "s", label = "$F_{exp}$ form E")
#plt.plot(z_F_th, F_th * 1e15, color = "k")
plt.plot(z_hist, f_th(z_hist) * 1e15, color = "k", label = "$F_{th}$")
plt.xlabel("$z$ (m)")
plt.ylabel("$F_{eq}$ (fN)")
plt.ylim((None,150))
plt.legend()
plt.show()
```

Now that we have verified that we corectly get the forces from the equilibrium probability density of height that permited us to measure the offset on the trajectory.

Now that everything is set up correctly we can try to measure the forces from the local drifts.

## Force measurement from the local drifts

In order to measure the forces from the local drifts we are gonna use some moving average magiK. This will permit us to have a quasi continuous measurement.

Bascicaly, the measurement is done in a few steps :

* Compute the volocity array `(x[1:] - x[0:-1])/dt `

* Sort the velocity and the height in the height order.

* Do a moving average on the velocity

* Compute the force : $F(z) = 6 \pi \eta(z) a \langle v \rangle $

* Apply the correction due to the Itö convention on the force $ \propto - \frac{dD}{D}$

```python
def movmean(x, w):
    """ Moving average function using convolution """
    return np.convolve(x, np.ones(w), 'same') / w

def movstd(x,w):
    mean = movmean(x,w)
    new_x = (x - mean)**2
    return np.sqrt(movmean(new_x, w))

def F_correction(alpha,z):
    """ Function to compute the correction on the force measurement due to the ito convention"""
    return - 4e-21 * alpha *(42*a*z*z + 24*a*a*z + 4*a*a*a)/(36*(z**4) + 66*a*(z**3) + 30*a*a*z*z + 4*(a**3)*z)
def eta(z):
    """visosity along the z axis -- utlity function to measure the forces"""
    result = ((6*z*z + 2*a*z) / (6*z*z + 9*a*z + 2*a*a))
    return 1/result
```

```python
z_drifts = raw_data[:,2] * 0.0513 * 1e-6
z_drifts = z_drifts - np.min(z_drifts) - offset
```

```python
# On calcul les vitesses:
dt = 1/60 # 60 fps
v = (z_drifts[1:] - z_drifts[:-1]) / dt
z_drifts = z_drifts[:-1] # Convention Itö -> on attribue la vitesse a z_1
v = v[z_drifts.argsort()] # on tri les vitesse en fonction de la hauteur
z_drifts = np.sort(z_drifts) # on tri les hauteurs.

```

We can also compute the error on the force :

\begin{equation}
Err_f = \sqrt{24 \pi k_\mathrm{B} T \eta_z(z) a / \tau_{box} }
\end{equation}

with $\tau_{box} = w_{average} / \mathrm{fps}$, $w_{average}$ being the width of the moving average.

```python
# Let's do some moving average magic ! ;)
w = 1500
a = 1.516e-6
F_drifts = 6 * np.pi * 0.001 * eta(z_drifts) * a * movmean(v, w)
F_drifts = F_drifts + F_correction(1, z_drifts)
#F_err = np.sqrt(24 * np.pi * 4e-21 *0.001*  eta(z_drifts) * a / (w*dt))
F_exp_err = 6 * np.pi * 0.001 * eta(z_drifts) * a * movstd(v, w)
z_th = np.linspace(10e-9, 4e-6, 1000)

F_err_th = np.sqrt(24 * np.pi * 4e-21 *0.001*  eta(z_th) * a / (w*dt))
F_drift_th = f_th(z_th)
```

```python
plt.semilogx(z_hist, F_eq * 1e15, "s", label = "$F_{exp}$ form E", zorder = 10)
plt.plot(z_drifts, F_drifts * 1e15, color = "tab:cyan", label = "local drift force")
plt.semilogx(z_th, F_drift_th  * 1e15, color = "k", zorder = 11, label="multifit force")
#plt.fill_between(z_drifts, F_drifts * 1e15 - F_exp_err*1e15, F_drifts * 1e15 + F_exp_err*1e15, alpha = 0.5, color = "tab:purple", label="drift force error")
plt.ylim((-40, 300))
plt.xlim((10e-9,None))
plt.legend()
plt.xlabel("$z$ (m)")
plt.ylabel("$F_{eq}$ (fN)")
plt.show()
```

same plot but with fewer points

```python
plt.plot(z_hist, F_eq * 1e15, "s", label = "$local form U", zorder = 10)
plt.errorbar(z_drifts[::2000],F_drifts[::2000] * 1e15,linestyle="", marker="o", label = "local drift force")
plt.semilogx(z_th, F_drift_th  * 1e15, color = "k", zorder = 11, label="multifit force")
plt.fill_between(z_th, F_drift_th  * 1e15 -F_err_th*1e15, F_drift_th  * 1e15 + F_err_th*1e15, alpha = 0.2, color ="k")
plt.plot(z_th, F_drift_th  * 1e15 -F_err_th*1e15, "--" ,color ="k")
plt.plot(z_th, F_drift_th  * 1e15 +F_err_th*1e15, "--", color ="k")

plt.ylim((-40, 300))
plt.xlim((10e-9,None))
plt.xlabel("$z$ (m)")
plt.ylabel("$F$ (fN)")
plt.legend()
plt.show()
```

```python
offset*1e6
```

    0.062

Finaly, everything seems to work, we do measure a sligtly higher from the drifts but it could simply be explain from a error on the offset of 5 nanometers.

```python
z = raw_data[:,2] * 0.0513
z = z - np.min(z) - offset*1e6

bins_center_pdf_z, widths, hist = logarithmic_hist(z, 39e-3, 3, num = 100, base=2)
z_F_exp = (bins_center_pdf_z - widths/2) * 1e-6
z_F_exp = z_F_exp[:-1]
fps = 60

def Dz_z(z,off):
    off =(off * 1e-6)
    z = z - off
    result = ((6*z*z + 2*a*z) / (6*z*z + 9*a*z + 2*a*a))
    return result

def vzz(Y, z,times=[1], method = "anti-Ito"):

    # We frist get all the different variables of the problem
    Y = Y * 1e-6
    z_z = z * 1e-6

    # We choose over wich time we want to comput the diffusion coefficient
    # Theoritically it should be the same for each time.
    # We initialize the variable used to store the results.
    # For the diffusion coefficient itself.
    vz = np.zeros((len(times), len(Y[:-1])))

    # For the error
    errvz = np.zeros((len(times), len(Y[:-1])))

    for n, i in enumerate(times):

        # Conpute the Delta x = x(Dt + t) - x(t) for given Dt -- same over y
        if method == "Ito" or method == "anti-Ito":
            Dzs = z_z[i:] - z_z[0:-i]
            Dzs = Dzs + F_correction(1,z[0:-i])/(6*np.pi*0.001*a*Dz_z(z[0:1],0))

        if method == "Stratanovich":
            Dzs = z_z[2*i:] - z_z[0:-2*i]
        #Dzs = z_z[0:-i] - z_z[i:]
        # Now for each z-bin we are going to measure the diffusion coefficient.

        for m in range(len(Y)-1):
            #print(len(Dzs))
            #print(len(z[i:]))
            # We take the Dx corresponding the actual bin
            if method == "anti-Ito":
                dz = Dzs[(z_z[i:] > Y[m]) & (z_z[i:] < Y[m+1])]
            if method == "Ito":
                dz = Dzs[(z_z[:-i] > Y[m]) & (z_z[:-i] < Y[m+1])]
            if method == "Stratanovich":
                dz = Dzs[(z_z[i:-i] > Y[m]) & (z_z[i:-i] < Y[m+1])]

            if method == "Ito" or method == "anti-Ito":
                vz[n,m] = np.mean(dz)/(i/fps)
            if method == "Stratanovich":
                vz[n,m] = np.mean(dz)/(2*i/fps)
            #errvz[n,m] = np.std(dz)/(i/fps)
            errvz[n,m] = len(dz)
    err_vz = np.nanmean(errvz, axis=0)
    vz = np.nanmean(vz, axis=0)

    return vz, err_vz

def F_corr(alpha,z):
    return -alpha * D(z) * 4e-21 * D_1_prime(z)

def D_1_prime(z):
    return (2 * r * (2 * r**2 + 12 * r * z + 21 *  z**2))/(2 * r** + 9 * r * z + 6 * z**2)**2

def D(z):
    return (6*z**2 + 9*r*z + 2*r**2) / (6*z**2 + 2 * r * z)

def F_corr(alpha,z):
    return -alpha * D(z) * 4e-21 * D_1_prime(z)
def F_corr(alpha,z):
    return - 4e-21 * alpha *(42*a*z*z + 24*a*a*z + 4*a*a*a)/(36*(z**4) + 66*a*(z**3) + 30*a*a*z*z + 4*(a**3)*z)

```

```python
a = 1.51e-6
vz_ito, err_ito = vzz(np.array([1,2,3,4,5,6]),z, method = "Ito")
#z_F_exp = bins_center_pdf_z[:-1] * 1e-6
F_exp_ito = 6 * np.pi  * 0.001 * a * vz_ito/ Dz_z(z_F_exp, 0) + F_corr(1,z_F_exp)
F_err_ito = 6 * np.pi  * 0.001 * a * err_ito/ Dz_z(z_F_exp, 0) + F_corr(1,z_F_exp)
```

```python
F
```

```python
plt.errorbar(z_F_exp, F_exp_ito * 1e15,yerr=F_err_ito*1e15/2,xerr=widths[:-1]/2*1e-6, marker = "o", linestyle="")
plt.plot(z_hist, F_eq * 1e15, "s", label = "$F_{exp}$ form E", zorder = 10)
plt.semilogx(z_th, F_drift_th  * 1e15, color = "k", zorder = 11, label="multifit force")
#plt.fill_between(z_th, F_drift_th  * 1e15 -F_err_th*1e15, F_drift_th  * 1e15 + F_err_th*1e15, alpha = 0.2, color ="k")
#plt.plot(z_th, F_drift_th  * 1e15 -F_err_th*1e15, "--" ,color ="k")
#plt.plot(z_th, F_drift_th  * 1e15 +F_err_th*1e15, "--", color ="k")

plt.ylim((-40, 300))
plt.xlim((10e-9,None))
plt.xlabel("$z$ (m)")
plt.ylabel("$F$ (fN)")
plt.legend()
plt.show()
```

```python
len(bins_center_pdf_z)
```

    99

# Froce error

Now we would like to compute the stochastic error on the force.

```python
def random_on_force():
    z = raw_data[:,2] * 0.0513
    z = z - np.min(z) - offset*1e6
    bins_center_pdf_z, widths, hist = logarithmic_hist(z, 39e-3, 3, num = 45, base=2)
    z = z + np.random.normal(0, 1e-3, len(z))
    z_F_exp = (bins_center_pdf_z - widths/2) * 1e-6
    z_F_exp = z_F_exp[:-1]
    a = 1.51e-6
    vz_ito, err_ito = vzz(bins_center_pdf_z, z, [1,2], method = "Ito")
    #z_F_exp = bins_center_pdf_z[:-1] * 1e-6
    F_exp_ito = 6 * np.pi  * 0.001 * a * vz_ito/ Dz_z(z_F_exp, 0) + F_corr(1,z_F_exp)
    return F_exp_ito, err_ito

```

```python
len(z_F_exp)
```

    98

```python
len(random_on_force()[0])
```

    43

```python
bins_center_pdf_z, widths, hist = logarithmic_hist(z, 39e-3, 3, num = 45, base=2)
z = z + np.random.normal(0, 1e-3, len(z))
z_F_exp = (bins_center_pdf_z - 1*widths/2) * 1e-6
z_F_exp = z_F_exp[:-1]

N = 100
all_F = np.ones((len(random_on_force()[0]),N))
all_err_F = np.ones((len(random_on_force()[0]),N))

for i in range(N):
    F, err = random_on_force()
    all_F[:,i] = F
    all_err_F[:,i] = err

```

```python
F_meaned = np.mean(all_F, axis=1)
F_err_tot = np.std(all_F, axis=1)

```

```python
T_boxes = np.mean(all_err_F, axis=1)
```

```python
#plt.plot(z_F_exp, T_boxes)
```

```python
f_th2 = f_th(z_F_exp)
F_err_th = np.sqrt(24 * np.pi * 4e-21 *0.001*  eta(z_F_exp) * a / (T_boxes*dt))

```

```python
[3]*4
```

    [3, 3, 3, 3]

```python
f_th
```

    <function __main__.<lambda>(z)>

```python
plt.figure(figsize=( cm2inch(16),cm2inch(9)))

plt.errorbar(z_F_exp, F_meaned*1e15, yerr=F_err_tot*1e15,xerr = widths[:-1]*1e-6/2 ,marker = "o", linestyle="", ecolor="k", capsize = 2, label = "Eq. (5.2.19)")
#plt.plot(z_hist, F_eq * 1e15, "s", label = "$F_{exp}$ form E", zorder = 10)
plt.fill_between(z_F_exp, f_th2  * 1e15 -F_err_th*1e15, f_th2  * 1e15 + F_err_th*1e15, alpha = 0.2, color ="k", label = "thermal noise range")
plt.plot(z_F_exp, f_th2  * 1e15 - F_err_th*1e15, "--" ,color ="k", linewidth = 2)
plt.plot(z_F_exp, f_th2  * 1e15 + F_err_th*1e15, "--" ,color ="k", linewidth = 2)
plt.plot(z_hist, F_eq * 1e15, "s", label = "Eq. (5.2.18)", zorder = 1)

plt.plot(z_F_exp, [np.mean( F_meaned[5:]  * 1e15)]*len(z_F_exp), color = "tab:red", linestyle= "--", linewidth = 2)
plt.semilogx(z_F_exp, f_th2 * 1e15, color = "k", zorder = 11, label="Eq. (5.2.21)")

#plt.plot(z_th, F_drift_th  * 1e15 +F_err_th*1e15, "--", color ="k")
plt.text(5e-7, 20, "$F_\mathrm{g} = -7$ fN", color="tab:red", fontsize ="medium" )
plt.ylim((-40, 200))
plt.xlim((35e-9,2.5e-6))
plt.xlabel("$z$ (m)", fontsize="medium")
plt.ylabel("$F$ (fN)", fontsize="medium")
plt.legend()
plt.savefig("figure_force_total.pdf")
plt.show()
```

```python
dict_force = {}
dict_force["z_F_exp"] = z_F_exp
dict_force["F_meaned"] = F_meaned
dict_force["widths"] = widths
dict_force["z_hist"] = z_hist
dict_force["f_th2"] = f_th2
dict_force["F_err_th"] = F_err_th
dict_force["F_err_tot"] = F_err_tot

import scipy
scipy.io.savemat("dict_force.mat", dict_force)

```

```python
z_hist
```

    array([4.06315748e-08, 4.40312392e-08, 4.77153552e-08, 5.17077230e-08,
           5.60341343e-08, 6.07225386e-08, 6.58032240e-08, 7.13090130e-08,
           7.72754741e-08, 8.37411520e-08, 9.07478164e-08, 9.83407320e-08,
           1.06568951e-07, 1.15485629e-07, 1.25148369e-07, 1.35619596e-07,
           1.46966957e-07, 1.59263756e-07, 1.72589435e-07, 1.87030080e-07,
           2.02678981e-07, 2.19637234e-07, 2.38014392e-07, 2.57929176e-07,
           2.79510240e-07, 3.02897003e-07, 3.28240548e-07, 3.55704600e-07,
           3.85466584e-07, 4.17718767e-07, 4.52669507e-07, 4.90544593e-07,
           5.31588707e-07, 5.76067002e-07, 6.24266819e-07, 6.76499538e-07,
           7.33102596e-07, 7.94441660e-07, 8.60912994e-07, 9.32946019e-07,
           1.01100608e-06, 1.09559747e-06, 1.18726667e-06, 1.28660587e-06,
           1.39425683e-06, 1.51091500e-06, 1.63733402e-06, 1.77433058e-06,
           1.92278972e-06])

```python

```

```python

```

```python

```

```python

```


# Nuclear Energetics

## Learning Objectives

- Define: exothermic and endothermic reactions
- Differentiate exothermic and endothermic reactions
- Calculate binding energies
- Recognize the noation for various binary reactions
- Recognize the physics in common binary reactions
- Define the relationship between the Q value, mass, and energy in a reaction
- Explain the role of energy conservation in binary reactions
- Explain the role of charge conservation in binary reactions
- Calculate Q values for various reactions

## Endothermic and Exothermic Reactions

In any reaction (nuclear, mechanical, chemical) energy can be either emitted or absorbed.

- When energy is emitted by the reaction, this is called **_exothermic_**.
- When energy is absorbed into the reaction, this is called **_endothermic_**.

If we consider the energy in relation to mass via Einstein's equivalence, then it's very clear that a change in the mass of reactants will result in a change in energy ($\Delta E = \Delta M c^2$).

\begin{align}
\mbox{reactants} &\rightarrow \mbox{products}\\
A + B + \cdots &\rightarrow C + D + \cdots\\
\Delta M = (M_A + M_B + \cdots) &- (M_C + M_D + \cdots)\\
\implies \Delta E &= \left[(M_A + M_B + \cdots) - (M_C + M_D + \cdots)\right]c^2\\
\end{align}

(credit: BBC)

## Exercise: Think-pair-share

Which of the following are exothermic and which are endothermic?

Easy ones:

- Boiling Water
- Lighting a match
- Freezing an ice cube
- Melting an ice cube
- Snow formation in the clouds
- Conversion of frost to water vapor on the grass

Harder ones:
- Formation of ion pairs (exothermic)
- Separation of ion pairs (endothermic)

The most important one:
- nuclear fission

# Binding Energy

For ease, let's imagine just two reactants and one product.

\begin{align}
\mbox{reactants} &\rightarrow \mbox{products}\\
A + B &\rightarrow C \\
\Delta M = (mass(A) + mass(B)) &- (mass(C))\\
\implies \Delta E &= \left[(mass(A) + mass(B) - (mass(C))\right]c^2\\
&= BE
\end{align}

## Nuclear and Atomic Masses

\begin{align}
M\left(^A_ZX\right) &= \mbox{rest mass of an atom}\\
m\left(^A_ZX\right) &= \mbox{rest mass of its nucleus}
\end{align}

All Z electrons must be bound to the atom, so the atomic and nuclear masses have the following relationship:

\begin{align}
M\left(^A_ZX\right) &=m\left(^A_ZX\right) + Zm_e - \frac{BE_{Ze}}{c^2}
\end{align}

Your book points out that it requires 13.6 eV to ionize the hydrogen atom. This electron binding energy represents a mass change of $BE_{1e/c^2} = 13.6 (eV)/9.315 \times 10^8 (eV/u) = 1.4 \times 10^{−8} u$.

## Binding Energy of the nucleus

A nucleus, with Z protons and N=A-Z neutrons will have a formation reaction that looks like:

\begin{align}
Z \mbox{ protons } + (A − Z) \mbox{ neutrons }\rightarrow \mbox{ nucleus}\left(^A_ZX\right) + BE.
\end{align}

It can be helpful to define the term $BE/c^2$:

\begin{align}
\frac{BE}{c^2} &=Zm_p +(A−Z)m_n − m\left(^A_ZX\right)\\
\end{align}

We can arrive at the nuclear binding energy from this:

\begin{align}
BE\left(^A_ZX\right) = \left[ZM(\left(^1_1H\right) + (A-Z)m_n - M\left(^A_ZX\right)\right]c^2
\end{align}

The above equation neglects the electron binding energies because:
- the terms tend to cancel
- electron binding energies are millions of times smaller than the nuclear binding energy.

### Exercise: Nuclear binding energy

From your book, example 4.1: What is the binding energy of an alpha particle?

\begin{align}
\frac{BE}{c^2} &=Zm_p +(A−Z)m_n − m\left(^A_ZX\right)\\
\implies\frac{BE}{c^2} &=\left[ZM(\left(^1_1H\right) + (A-Z)m_n - M\left(^A_ZX\right)\right]\\
 &=\left[2M(\left(^1_1H\right) + 2m_n - M\left(^4_2He\right)\right]\\
 &=\left[2\left(1.0078250\right) + 2\left(1.0086649\right) - 4.0026032\right]\\
\end{align}

Thus:

\begin{align}
BE\left(^4_2 He\right) &= \frac{BE}{c^2}\times 931.5 (MeV/u) \\
&= 28.30 MeV.
\end{align}

## Binding Energy per nucleon

The BE/A versus A curve immediately suggests two ways to extract energy from the nucleus.

Binding energy curve **(average binding energy per nucleon in MeV against number of nucleons in nucleus)** for a number of relatively common (abundant) isotopes (not chosen systematically; almost anything with an occurence of over .2 was chosen though a few exceptions are in there, such as U235). A few important ones for the purposes of nuclear fusion and nuclear fission are marked, as well as iron-56, which sits at the highest point on this graph and cannot yield energy from fusion or fission.

From your book (Shultis and Faw)

> Most isotopes of an element are radioactive, i.e., their nuclei are unstable. To better understand what makes a nucleus stable, it is instructive to plot the BE per nucleon, BE/A, of all the isotopes of an element. For example, if one calculates the BE/A for all the isotopes of calcium (Z = 20), as is done in Example 4.2 for one isotope, and plots BE/A versus the neutron number N, a plot like that shown in Fig. 4.2 is obtained. The BE/A rapidly rises as N increase from A = 14 and reaches a broad maximum before rapidly decreasing as N increases above 30. It is in the broad maximum where the BE/A is highest that one finds the stable isotopes. The reason for the zig-zag shape in the maximum region is due to the fact that an isotope with an even number of neutrons tends to have a higher BE/A compared

### Fusion

**Two light nuclei $\longrightarrow$ one heavy nucleus**

\begin{align}
^{2}_1H + ^2_1H \longrightarrow ^{4}_2He
\end{align}

### Fission
**One heavy nucleus $\longrightarrow$ two lighter nuclei**

\begin{align}
^{235}_{92}U + ^1_0n \longrightarrow \left(^{236}_{92}U\right)^*  \longrightarrow ^{139}_{56}Ba +  ^{94}_{36}Kr + 3^1_0n
\end{align}

## Nucleon Separation Energy

The energy required to remove a single nucleon from a nucleus is called "separation energy". Consider the addition of a single neutron to form the nucleus of $^A_ZX$, i.e.,

\begin{align}
^{A-1}_ZX + ^1_0n \rightarrow ^{A}_ZX.
\end{align}

$S_n\left(^A_ZX\right)$ is the energy released in this reaction and is equivalent to the energy required to remove (or separate) a single neutron from the nucleus $^A_ZX$.

\begin{align}
S_n\left(^A_ZX\right) &= [m\left(^{A-1}_ZX\right) + m_n − m\left(^{A}_ZX\right)]c^2 \\
 &\simeq [M\left(^{A-1}_ZX\right) + m_n − M\left(^{A}_ZX\right)]c^2.
\end{align}

If we make the substition for nuclear binding energy:

\begin{align}
BE\left(^A_ZX\right) = \left[ZM(\left(^1_1H\right) + (A-Z)m_n - M\left(^A_ZX\right)\right]c^2
\end{align}

We can arrive at the separation energy:

\begin{align}
S_n\left(^A_ZX\right) &= BE\left(^A_ZX\right) - BE\left(^{A-1}_ZX\right)
\end{align}

## Exercise: Nucleon Separation Energy
(Example 4.3 in Shultis and Faw)

What is the binding energy of the last neutron in $^{16}_8O$? This is the energy released in the reaction:
\begin{align}
^{15}_8O + ^1_0n \rightarrow ^{16}_8O
\end{align}

So, we can use the equation above and plug in the mass values we can find in the book appendix.

\begin{align}
S_n\left(^{A}_ZX\right) &= [m\left(^{A-1}_ZX\right) + m_n − m\left(^{A}_ZX\right)]c^2 \\
 &\simeq [M\left(^{A-1}_ZX\right) + m_n − M\left(^{A}_ZX\right)]c^2\\
\implies S_n &= [m\left(^{15}_8O\right) + m_n − m\left(^{16}_8O\right)]c^2 \\
 &\simeq [M\left(^{15}_8X\right) + m_n − M\left(^{16}_8X\right)]c^2\\
 &=[15.0030654 + 1.00866492 − 15.9949146] u \times 931.5 MeV/u\\
 &= 15.66 MeV.
\end{align}

This is a very high value for a single nucleon.

### Think-Pair-Share

What does this high value mean about the comparative stability of $^{16}_8O$ and $^{15}_8O$?

### $S_p$

The equation for the separation energy of the proton is very similar to the separation energy of the neutron.

\begin{align}
S_p\left(^{A}_ZX\right) &= [m\left(^{A-1}_{Z-1}X\right) + m_p − m\left(^{A}_ZX\right)]c^2 \\
 &\simeq [M\left(^{A-1}_{Z-1}X\right) + M\left(^1_1H\right) − M\left(^{A}_ZX\right)]c^2\\
\end{align}

## Exercise: $BE_e$ in $^1_1H$

A problem example from Shultis and Faw follows below.

**Energies of chemical reactions can typically not be calculated by finding the difference between the masses of the reactants and the products because the mass must be known to 10 or more significant figures.**

However, the mass of the proton and hydrogen atom are known to 10 significant figures. Let's estimate the binding energy of the electron $BE_e$ in the $^1_1H$ atom and compare this result to what the Bohr model predicts.

Discuss this comparison.

The binding energy reaction can be written as

\begin{align}
^{1}_1p + ^0_{-1}e \rightarrow ^{1}_1H + BE_e
\end{align}

We use mass values found in Table A.1 and Appendix B to find the binding energy:

\begin{align}
BE_e &= \left[m_p + m_e - M\left(^1_1H\right)\right]\left(u\right) \times 931.5(MeV/u)\\
 &= \left[1.0072764669 + 5.48579909\times 10^{-4} - 1.0078250321\right]\left(u\right) \times 931.5(MeV/u)\\
&= 1.3887 \times 10^{−5} MeV\\
&= \boxed{13.887 eV}.
\end{align}

Recall that $BE_e = 13.606 eV$. Although the estimate of 13.887 is correct to 2 significant figures. To get a better estimate, the proton and hydrogen atomic masses must be known to at least 13 significant figures, an accuracy beyond present day technology.

## Binary Nuclear Reactions

We will refer to the reaction $x+X\longrightarrow Y+ y$ as :

\begin{align}
(x, y)
\end{align}

which is shorthand for $X(x,y)Y$. The very long form of this reaction is:

\begin{align}
^{A}_{Z}X + ^{A}_{Z}x \longrightarrow ^{A}_{Z}Y + ^{A}_{Z}y
\end{align}

### $(\alpha, p)$

This  was first seen in air by Rutherford. Protons were produced when alpha particles were absorbed by nitrogen in the air to make oxygen. That example follows.

\begin{align}
^{4}_{2}He + ^{14}_{7}N \longrightarrow ^{17}_{8}O + ^{1}_{1}H
\end{align}

### $(\alpha, n)$

This  was first seen with beryllium by Chadwick. Neutrons were produced when alpha particles were absorbed by beryllium in the air to make carbon. That example follows.

\begin{align}
^{4}_{2}He + ^{9}_{4}Be \longrightarrow ^{12}_{6}C + ^{1}_{0}n
\end{align}

### $(\gamma, n)$

Neutrons can also be produced when very energetic photons strike a nucleus. An example follows.

\begin{align}
^\gamma + ^{2}_{1}H \longrightarrow ^{1}_{1}H + ^{1}_{0}n
\end{align}

### $(p, \gamma)$

This is typically called "radiative capture" of a proton.  An example follows.

\begin{align}
^{1}_{1}H + ^{7}_{3}Li \longrightarrow ^{8}_{4}Be + \gamma
\end{align}

Interestingly, in this example, the resulting beryllium nucleus is poorly bound and becomes two alphas.

### $(\gamma, \alpha n)$

Sometimes, more than two products can be produced. An example follows.

\begin{align}
\gamma + ^{17}_{8}O \longrightarrow ^{12}_{6}C + ^{4}_{2}He + ^{1}_{0}n
\end{align}

### $(n, p)$

Fast neutrons are very common in nculear reactor cores and can produce liberate protons.  An example follows.

\begin{align}
^{1}_{0}n + ^{16}_{8}O \longrightarrow ^{16}_{7}N + ^{1}_{1}p
\end{align}

### $(n, \gamma)$

Most free neutrons in a reactor are fated to be radiatively captured.

\begin{align}
^{1}_{0}n + ^{238}_{92}U \longrightarrow ^{239}_{92}U + \gamma
\end{align}

## Q-Value For a Reaction

Energy is conserved in nuclear reactions. Specifically, the _total energy_ is conserved.

The total energy, recall, includes rest mass energy as well as kinetic energy.

\begin{align}
\sum_i \left[E_i + m_ic^2\right] &= \sum_i\left[E'_i + m'_ic^2\right]\\
\mbox{where }&\\
E_i &= \mbox{the kinetic energy of the ith initial particle }\\
E'_i &=\mbox{the kinetic energy of the ith final particle }\\
m_i &= \mbox{rest mass of the ith initial particle}\\
m'_i &= \mbox{rest mass of the ith final particle}\\
\end{align}

Changes in kinetic energy between the initial and final states must be balanced by equivalent changes in total rest mass. **The Q-value is the kinetic energy gained in the reaction.**

\begin{align}
Q &= (\mbox{KE of final particles}) − (\mbox{KE of initial particles})\\
 &= \left(\sum_iE'_i\right) − \left(\sum_iE_i\right) \\
\end{align}

**The Q-value is equivalently the rest mass energy lost in the reaction.**

\begin{align}
Q &= (\mbox{rest mass of initial particles})c^2 − (\mbox{rest mass of final particles})c^2\\
  &= \left(\sum_im_i\right)c^2 − \left(\sum_im'_i\right)c^2 \\
\end{align}

### Think-pair-share: Exothermic or Endothermic

If $Q<0$, is the reaction exothermic or endothermic?

### Q-value in binary reactions

For an $X(x, y)Y$ reaction:

\begin{align}
Q &= \left(E_y + E_Y \right) − \left(E_x + E_X\right) \\
  &= \left( m_x + m_X\right)c^2 − \left( m_y + m_Y\right)c^2 \\
\end{align}

**If proton numbers (and therefore electron masses) are conserved,** we can use the neutral atom masses and simplify this to:

\begin{align}
Q &= \left(E_y + E_Y \right) − \left(E_x + E_X\right) \\
  &= \left( M_x + M_X\right)c^2 − \left( M_y + M_Y\right)c^2 \\
\end{align}

### Q-value in decay reactions
For a radioactive decay reaction:

\begin{align}
X\longrightarrow Y+ y
\end{align}

The corresponding Q-value is:
\begin{align}
Q&=(E_y +E_Y)\\
&=[m_X −(m_y +m_Y )]c^2 \\
\end{align}

Note that radioactive decay, being spontaneous, is **always exothermic**. So, $Q>0$ for all decay reactions.

### Think-pair-share

- Can we make the same simplification as above for all decay reactions?
- If yes, why?
- If not, why not?

### Total Charge Conservation

Charge is always conserved in nuclear reactions. Thus, we might get into trouble with reactions that drive a change in electron number. The mass of the electron should be managed carefully in these cases. In an $n, p$ reaction, a proton is ejected from the nucleus by the incident neutron. During this process, an orbital electron is usually also lost from the resulting product atom.  Thus, we must include an extra electron in the equation.

**Does the electron $_{-1}^0e$ belong on the right hand side (products) or the left hand side (reactants) of the following reaction?**

\begin{align}
^1_0n + ^{16}_8O \longrightarrow ^{16}_7N + ^1_1p
\end{align}

### Special case for changing proton number

The above procedure is further complicated in cases when the number of protons and the number of neutrons are not conserved. These reactions involve the nuclear weak force.

**The nuclear _weak force_ alters neutrons into protons and vice versa, usually inside the nucleus.**

The details are provided in an example in your book. Effectively, the neutrinos which moderate this reaction must be considered among the products and reactants.

## Special case for exited products

Many of the reactions we're  most interested in as nuclear engineers involve an intermediate excited compound nucleus.

Let's consider Example 4.6 in your book regarding the reaction $^{10}B + ^1_0n \longrightarrow \left(^7Li\right)^*$. In short hand, this is an $(n,\alpha)$ reaction. Specifically,  $^{10}B(n,\alpha)^7Li^∗$. The product, $\left(^7Li\right)^*$ is left in an excited state. Specifically it is left 0.48 MeV above its ground state.

The ordinary neutral atomic masses approach would look like this:

\begin{align}
Q &= \left[m_n + M\left(^{10}_5B\right) −  M\left(^{4}_2He\right) - M\left(^{7}_3Li\right)^*\right]c^2 \\
\end{align}

But, the mass tabulated for $M\left(^{7}_3Li\right)$ is the ground state mass. So if we want to use that ground state mass to arrive at the solution, the extra energy contributing to the excited state needs to be accounted for.

\begin{align}
Q &= \left[m_n + M\left(^{10}_5B\right) −  M\left(^{4}_2He\right) - M\left(^{7}_3Li\right)\right]c^2 -0.48MeV\\
\end{align}

## An aside on Florence and Mangkhut

![https://heavyeditorial.files.wordpress.com/2018/09/power-reactors-operating.png?w=708&h=531](
https://heavyeditorial.files.wordpress.com/2018/09/power-reactors-operating.png?w=708&h=531])

[Duke shuts down Brunswick ahead of Florence](https://www.reuters.com/article/us-storm-florence-duke-nuclear/duke-starts-to-shut-north-carolina-brunswick-nuclear-plant-ahead-of-florence-idUSKCN1LT1ZT)

[Yangjiang and Taishan in path of Typhoon Mangkhut](https://www.scmp.com/news/china/society/article/2164363/chinese-nuclear-power-plant-path-super-typhoon-mangkhut)

```python

```


## Inverse dynamics

```python
# %load imports.py
%load_ext autoreload
%autoreload 2
%config Completer.use_jedi = False  ## (To fix autocomplete)

import pandas as pd
from src.models.vmm import ModelSimulator
import matplotlib.pyplot as plt
import matplotlib
plt.style.use('presentation')
matplotlib.rcParams["xtick.labelsize"] = 16
from src.visualization.plot import track_plots, plot, captive_plot
import kedro
import numpy as np
import os.path
import anyconfig

from myst_nb import glue
from src.symbols import *
import src.symbols as symbols
from src.system_equations import *

from IPython.display import display, Math, Latex, Markdown
from sympy.physics.vector.printing import vpprint, vlatex

from src.models.regression import MotionRegression

from src.parameters import df_parameters
from src.substitute_dynamic_symbols import run
from src.models.diff_eq_to_matrix import DiffEqToMatrix
p = df_parameters["symbol"]
import statsmodels.api as sm

# Read configs:
conf_path = os.path.join("../../conf/base/")
runs_globals_path = os.path.join(
    conf_path,
    "runs_globals.yml",
)

runs_globals = anyconfig.load(runs_globals_path)
model_test_ids = runs_globals["model_test_ids"]

join_globals_path = os.path.join(
    conf_path,
    "join_globals.yml",
)

joins = runs_globals["joins"]
join_runs_dict = anyconfig.load(join_globals_path)

globals_path = os.path.join(
    conf_path,
    "globals.yml",
)
global_variables = anyconfig.load(globals_path)

vmm_names = global_variables["vmms"]
only_joined = global_variables[
    "only_joined"
]  # (regress/predict with only models from joined runs)S

vmms = {}
for vmm_name in vmm_names:
    vmms[vmm_name] = catalog.load(vmm_name)

```

```python
%reload_kedro
vmm_name = 'vmm_martins_simple'
vmm = vmms[vmm_name]
data = pd.read_csv('example.csv', index_col=0)
added_masses = catalog.load("added_masses")
model = catalog.load(f"{ vmm_name}.motion_regression.joined.model")
initial_parameters = catalog.load("initial_parameters")
model.parameters=initial_parameters

regression = MotionRegression(
        vmm=vmm,
        data=data,
        added_masses=added_masses,
        prime_system=model.prime_system,
        ship_parameters=model.ship_parameters,
        #exclude_parameters={"Xthrust": 1.0, "Ydelta": 1},
    )
```

    2022-03-28 16:49:48,958 - kedro.framework.session.store - INFO - `read()` not implemented for `SQLiteStore`. Assuming empty store.
    2022-03-28 16:49:50,577 - root - INFO - ** Kedro project wPCC_pipeline
    2022-03-28 16:49:50,579 - root - INFO - Defined global variable `context`, `session`, `catalog` and `pipelines`
    2022-03-28 16:49:50,589 - root - INFO - Registered line magic `run_viz`
    2022-03-28 16:49:50,619 - kedro.io.data_catalog - INFO - Loading data from `added_masses` (YAMLDataSet)...
    2022-03-28 16:49:50,621 - kedro.io.data_catalog - INFO - Loading data from `vmm_martins_simple.motion_regression.joined.model` (PickleDataSet)...
    2022-03-28 16:49:50,628 - kedro.io.data_catalog - INFO - Loading data from `initial_parameters` (YAMLDataSet)...

    e:\dev\wpcc\src\models\regression.py:106: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
      self.exclude_parameters = pd.Series(exclude_parameters)
    <string>:2: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
    e:\dev\wpcc\src\models\regression.py:470: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
      pd.Series({key: 0 for key in self.connected_parameters_Y.keys()})
    e:\dev\wpcc\src\models\regression.py:484: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
      self.std = pd.Series()

```python
eq_system
```

$\displaystyle \left[\begin{matrix}- X_{\dot{u}} + m & 0 & 0\\0 & - Y_{\dot{v}} + m & - Y_{\dot{r}} + m x_{G}\\0 & - N_{\dot{v}} + m x_{G} & I_{z} - N_{\dot{r}}\end{matrix}\right] \left[\begin{matrix}\dot{u}\\\dot{v}\\\dot{r}\end{matrix}\right] = \left[\begin{matrix}m r^{2} x_{G} + m r v + \operatorname{X_{D}}{\left(u,v,r,\delta,thrust \right)}\\- m r u + \operatorname{Y_{D}}{\left(u,v,r,\delta,thrust \right)}\\- m r u x_{G} + \operatorname{N_{D}}{\left(u,v,r,\delta,thrust \right)}\end{matrix}\right]$

```python
solution = sp.solve(eq_system.doit(),X_D,Y_D,N_D, dict=True)[0]

eq_XD = sp.Eq(X_D, solution[X_D])
eq_YD = sp.Eq(Y_D, solution[Y_D])
eq_ND = sp.Eq(N_D, solution[N_D])

display(eq_XD)
display(eq_YD)
display(eq_ND)

```

$\displaystyle \operatorname{X_{D}}{\left(u,v,r,\delta,thrust \right)} = - X_{\dot{u}} \dot{u} + \dot{u} m - m r^{2} x_{G} - m r v$

$\displaystyle \operatorname{Y_{D}}{\left(u,v,r,\delta,thrust \right)} = - Y_{\dot{r}} \dot{r} - Y_{\dot{v}} \dot{v} + \dot{r} m x_{G} + \dot{v} m + m r u$

$\displaystyle \operatorname{N_{D}}{\left(u,v,r,\delta,thrust \right)} = I_{z} \dot{r} - N_{\dot{r}} \dot{r} - N_{\dot{v}} \dot{v} + \dot{v} m x_{G} + m r u x_{G}$

```python
display(vmm.X_qs_eq)
display(vmm.Y_qs_eq)
display(vmm.N_qs_eq)

```

$\displaystyle \operatorname{X_{D}}{\left(u,v,r,\delta,thrust \right)} = X_{deltadelta} \delta^{2} + X_{rr} r^{2} + X_{thrust} thrust + X_{u} u + X_{vr} r v$

$\displaystyle \operatorname{Y_{D}}{\left(u,v,r,\delta,thrust \right)} = Y_{delta} \delta + Y_{r} r + Y_{thrustdelta} \delta thrust + Y_{ur} r u + Y_{u} u + Y_{v} v$

$\displaystyle \operatorname{N_{D}}{\left(u,v,r,\delta,thrust \right)} = N_{delta} \delta + N_{r} r + N_{thrustdelta} \delta thrust + N_{ur} r u + N_{u} u + N_{v} v$

```python
subs = [(value, key ) for key,value in p.items()]
subs.append((u1d,'u1d'))
subs.append((v1d,'v1d'))
subs.append((r1d,'r1d'))

eq = eq_XD.subs(subs)
lambda_X_D = sp.lambdify(list(eq.rhs.free_symbols), eq.rhs)

eq = eq_YD.subs(subs)
lambda_Y_D = sp.lambdify(list(eq.rhs.free_symbols), eq.rhs)

eq = eq_ND.subs(subs)
lambda_N_D = sp.lambdify(list(eq.rhs.free_symbols), eq.rhs)
```

```python
df_captive = data.copy()
df_captive_prime = model.prime_system.prime(df_captive, U=data['U'])

df_captive_prime['fx'] = run(lambda_X_D,
                             inputs=df_captive_prime,
                             **model.ship_parameters_prime,
                             **added_masses)

df_captive_prime['fy'] = run(lambda_Y_D,
                             inputs=df_captive_prime,
                             **model.ship_parameters_prime,
                             **added_masses)

df_captive_prime['mz'] = run(lambda_N_D,
                             inputs=df_captive_prime,
                             **model.ship_parameters_prime,
                             **added_masses)

```

```python
Y_D_ = sp.symbols('Y_D')
eq = vmm.Y_qs_eq.subs(Y_D,Y_D_)
diff_eq_Y = DiffEqToMatrix(eq, label=Y_D_, base_features=[u,v,r,delta,thrust])

X_Y,y_Y = diff_eq_Y.calculate_features_and_label(data=df_captive_prime, y=df_captive_prime['fy'])

model_Y = sm.OLS(y_Y, X_Y)
result_Y = model_Y.fit()
```

    e:\dev\wpcc\src\models\diff_eq_to_matrix.py:63: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
      self.exclude_parameters = pd.Series(exclude_parameters)

```python
N_D_ = sp.symbols('N_D')
eq = vmm.N_qs_eq.subs(N_D,N_D_)
diff_eq_N = DiffEqToMatrix(eq, label=N_D_, base_features=[u,v,r,delta,thrust])

X_N,y_N = diff_eq_N.calculate_features_and_label(data=df_captive_prime, y=df_captive_prime['mz'])

model_N = sm.OLS(y_N, X_N)
result_N = model_N.fit()
```

```python
X_D_ = sp.symbols('X_D')
eq = vmm.X_qs_eq.subs(X_D,X_D_)
diff_eq_X = DiffEqToMatrix(eq, label=X_D_, base_features=[u,v,r,delta,thrust], exclude_parameters={'Xthrust':model.parameters['Xthrust']})

X_X,y_X = diff_eq_X.calculate_features_and_label(data=df_captive_prime, y=df_captive_prime['fx'])

model_X = sm.OLS(y_X, X_X)
result_X = model_X.fit()
```

```python
df_parameters_X = pd.DataFrame(pd.Series({key:value for key,value in model.parameters.items() if key[0]=='X' and value !=0}, name='real'))
df_parameters_X['regression'] = result_X.params
df_parameters_X.dropna(inplace=True)
df_parameters_X.index = p[df_parameters_X.index].apply(lambda x: "$%s$" % str(x).replace('delta',r'\delta'))
df_parameters_X.index.name = ''

df_parameters_Y = pd.DataFrame(pd.Series({key:value for key,value in model.parameters.items() if key[0]=='Y' and value !=0}, name='real'))
df_parameters_Y['regression'] = result_Y.params
df_parameters_Y.dropna(inplace=True)
df_parameters_Y.index = p[df_parameters_Y.index].apply(lambda x: "$%s$" % str(x).replace('delta',r'\delta').replace('thrust','T'))
df_parameters_Y.index.name = ''

df_parameters_N = pd.DataFrame(pd.Series({key:value for key,value in model.parameters.items() if key[0]=='N' and value !=0}, name='real'))
df_parameters_N['regression'] = result_N.params
df_parameters_N.dropna(inplace=True)
df_parameters_N.index = p[df_parameters_N.index].apply(lambda x: "$%s$" % str(x).replace('delta',r'\delta').replace('thrust','T'))
df_parameters_N.index.name = ''

```

```python
fig,axes=plt.subplots(ncols=3)
ax=axes[0]
df_parameters_X.plot.bar(ax=ax)
ax=axes[1]
df_parameters_Y.plot.bar(ax=ax)
ax.get_legend().set_visible(False)
ax=axes[2]
df_parameters_N.plot.bar(ax=ax)
plt.tight_layout()
ax.get_legend().set_visible(False)

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```


```python
import numpy as np
import scipy.signal
import matplotlib.pyplot as plt
from time import time

"""
PointEnv from rllab
The goal is to control an agent and get it to the target located at (0,0).
At each timestep the agent gets its current location (x,y) as observation,
takes an action (dx,dy), and is transitioned to (x+dx, y+dy).
"""

class PointEnv():
    def reset(self):
        self._state = np.random.uniform(-1, 1, size=(2,))
        state = np.copy(self._state)
        return state

    def step(self, action):
        action = np.clip(action, -1, 1)
        self._state = self._state + 0.1 * action
        x, y = self._state
        reward = -(x ** 2 + y ** 2) ** 0.5 - 0.02 * np.sum(action ** 2)
        done = abs(x) < 0.01 and abs(y) < 0.01
        next_state = np.copy(self._state)
        return next_state, reward, done
```

```python
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.colors import BoundaryNorm
from matplotlib.ticker import MaxNLocator
import numpy as np

# make these smaller to increase the resolution
dx, dy = 0.05, 0.05

# generate 2 2d grids for the x & y bounds
y, x = np.mgrid[slice(-5, 5 + dy, dy),
                slice(-5, 5 + dx, dx)]

z = -np.sqrt(x**2 + y**2)

# x and y are bounds, so z should be the value *inside* those bounds.
# Therefore, remove the last value from the z array.
z = z[:-1, :-1]
levels = MaxNLocator(nbins=15).tick_values(z.min(), z.max())

# pick the desired colormap, sensible levels, and define a normalization
# instance which takes data values and translates those into levels.
cmap = plt.get_cmap('spring')
norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True)

fig, ax0 = plt.subplots(nrows=1)

im = ax0.pcolormesh(x, y, z, cmap=cmap, norm=norm)
fig.colorbar(im, ax=ax0)
ax0.set_title('Reward')

plt.show()
```

### Continuous Policy Gradient Theorem

Let's examine the policy gradient theorem for continuous domains. Recall the reinforcement learning objective, which we write here in full:
\begin{align}
J(\theta)=\int p_0(s)\int \pi_\theta(a\vert s) Q(a,s) da ds. \label{eq:rl_objective}
\end{align}
Just like in the discrete case (https://papers.nips.cc/paper/1713-policy-gradient-methods-for-reinforcement-learning-with-function-approximation.pdf , (Sutton et al. 2000)), we can take derivatives of objective with respect to the policy parameters $\theta$, yielding:
\begin{align}
\nabla_\theta J(\theta)=\int\pi_\theta(a_t\vert s_t)\int \nabla_\theta\pi_\theta(a\vert s) Q(a,s) da ds ,\label{eq:policy_gradient_theorem}
\end{align}
where $\pi_\theta(a_t\vert s_t)$ is the discounted ergodic occupancy measure. Using the log-derivative trick, we write \cref{eq:policy_gradient_theorem} as an expectation over actions:
\begin{align}
\nabla_\theta J(\theta)=&\int\pi_\theta(a_t\vert s_t)\int\pi_\theta(a\vert s) \nabla_\theta\log\pi_\theta(a\vert s) Q(a,s) da ds,\\ =&\mathbb{E}_{s\sim\pi_\theta(a_t\vert s_t),a\sim\pi_\theta(a\vert s)}\Big[\nabla_\theta\log\pi_\theta(a\vert s) Q(a,s)\Big].
\end{align}
We then approximate the expectation by using $N$ samples from the environment under our policy $\pi_\theta(a\vert s)$, giving:
\begin{align}
\nabla_\theta J(\theta)\approx&\frac{1}{N}\sum_{n=0}^{N-1}\sum_{t=0}^{T_N}\nabla_\theta\log\pi_\theta(a_t\vert s_t) Q(a_t,s_t).
\end{align}

### REINFORCE and Baselines

Using a Monte-Carlo approximator for the action-value function, $Q(a_t,s_t)\approx R_t\triangleq\sum_{i=t}^T r(a_i,s_i)$, we recover the REINFORCE (http://www-anw.cs.umass.edu/~barto/courses/cs687/williams92simple.pdf ,(Williams 1992)) algorithm:
\begin{align}
\nabla_\theta J(\theta)\approx&\frac{1}{N}\sum_{n=0}^{N-1}\sum_{t=0}^{T_N}\nabla_\theta\log\pi_\theta(a_t\vert s_t) R_t.
\end{align}

### Exercise I.1: Implementing REINFORCE

We choose our policy to be a linear Gaussian policy with parameters $\theta$. Given state $s$, we can define some features $\phi(s)$ and sample an action $a \sim N(\phi(s)^T \theta, \sigma^2)$. Our PointEnv environment is simple enough that we can use $\phi(s) = s$. Note that $\sigma^2$ can also depend on $s$, but we have kept it constant in Part I for simplicity. Policy gradient algorithms use the update rule $\theta' = \theta + \alpha \nabla_\theta J(\pi)$, where $\alpha$ is the learning rate and $J(\pi)$ is the expected return of the policy.

PROBLEM I.1) In the function "get\_action\_and\_grad()", sample an action from $\pi_\theta(a\vert s)$ and compute the corresponding value of $\nabla_\theta\log\pi_\theta(a\vert s)$.

```python
class Gauss_Policy():
    def __init__(self):
        self.action_dim = 2
        self.theta = 0.5 * np.ones(4)
        # theta here is a length 4 array instead of a matrix for ease of processing
        # Think of treating theta as a 2x2 matrix and then flatenning it, which gives us:
        # action[0] = state[0]*[theta[0], theta[1]]
        # action[1] = state[1]*[theta[2], theta[3]]

    def get_action_and_grad(self, state):
        # Exercise I.1:
        mean_act = ...
        sampled_act = ...
        grad_log_pi = ...
        return sampled_act, grad_log_pi
```

```python
# This function collects some trajectories, given a policy
def gather_paths(env, policy, num_paths, max_ts=500):
    paths = []
    for i in range(num_paths):
        ts = 0
        states = []
        act = []
        grads = []
        rwd = []
        done = False
        s = env.reset()
        while not done and ts < max_ts:
            a, grad_a = policy.get_action_and_grad(s)
            next_s, r, done = env.step(a)
            states += [s]
            act += [a]
            rwd += [r]
            grads += [grad_a]
            s = next_s
            ts += 1
        path = {'states': np.array(states),
                'actions': np.array(act),
                'grad_log_pi': np.array(grads),
                'rwd': np.array(rwd)}
        paths += [path]
    return paths
```

### Exercise I.2: Using Baselines to Reduce Variance

We can subtract a baseline $V(s)$ from the action-value function, which does not affect the overall gradient, but will reduce variance in the gradient update. To see why, as $V_\omega(s)\triangleq\mathbb{E}_{a\sim\pi_\theta(a\vert s)}[Q_\omega(a,s)]$, the quantity $Q_\omega(a,s)-V\omega(s)$ will only be positive for $Q_\omega(a,s)>V_\omega(s)$, hence the gradient updates will therefore only be scaled by a positive value when an action has greater reward than the average return. For these reasons, we call $A_\omega(a,s)=Q_\omega(a,s)-V_\omega(s)$ the advantage. For REINFORCE, $A_\omega(a_t,s_t)=R_t-V_\omega(s_t)$. Just like we define features $\phi(s)$ for the policy, we can define some features $\psi(s,t)$ for the value function. Our features are going to be $\phi(s,t)=[s, s^2, t, t^2, t^3, 1]$ and we will approximate the value function as a linear function of these features, $V_\omega (s_t)=\omega^T\phi(s,t)$

PROBLEM I.2a) Given some sampled trajectories, fitting the value function is a linear regression problem.  The targets for our linear regression problem will therefore be the returns. The features have been implemented in the function "baselines()" you are required to compute the regression coefficients. Hint: The function "np.linalg.lstsq" may be of use.

PROBLEM I.2b) Now calculate the value for each state in \code{path} using the newly learnt coefficients and save it to "path[`value']"

```python
def baseline(paths):
    path_features = []
    for path in paths:
        s = path["states"]
        l = len(path["rwd"])
        al = np.arange(l).reshape(-1, 1) / 100.0
        path_features += [np.concatenate([s, s ** 2, al, al ** 2, al ** 3, np.ones((l, 1))], axis=1)]
    ft = np.concatenate([el for el in path_features])
    targets = np.concatenate([el['returns'] for el in paths])

    # Exercise I.2(a): Compute the regression coefficents
    coeffs = ...
    # Exercise I.2(b): Calculate the values for each state
    for i, path in enumerate(paths):
        path['value'] = ...
```

```python
def process_paths(paths, discount_rate=1):
    grads = []
    for path in paths:
        # Exercise I.3a: Implement the discounted return
        # Hint: This can be done in one line using lfilter from scipy.signal,
        # but it might be much easier to write a separate function for this
        path['returns'] = scipy.signal.lfilter([1], [1, float(-discount_rate)], path['rwd'][::-1], axis=0)[::-1]
    baseline(paths)
    for path in paths:
        path['adv'] = path['returns'] - path['value']
        rets_for_grads = np.atleast_2d(path['adv']).T
        rets_for_grads = np.repeat(rets_for_grads, path['grad_log_pi'].shape[1], axis=1)
        path['grads'] = path['grad_log_pi'] * rets_for_grads
        grads += [np.sum(path['grads'], axis=0)]
    grads = np.sum(grads, axis=0) / len(paths)
    return grads

```

```python
# Run algo
env = PointEnv()
alpha = 0.01
traj_len = 50
perf_stats = []

def run_algo(env, alpha, gamma, traj_len, num_itr=200, runs=10):
    rwd = np.zeros((num_itr, runs))
    for st in range(runs):
        policy = Gauss_Policy()
        for i in range(num_itr):
            paths = gather_paths(env, policy, max_ts=traj_len, num_paths=5)
            rwd[i, st] = np.mean([np.sum(path['rwd']) for path in paths])
            grads = process_paths(paths, discount_rate=gamma)
            policy.theta += alpha * grads
    perf_stats = {'gamma': gamma,
                  'mean_rwd': np.mean(rwd, axis=1),
                  'std_err': np.std(rwd, axis=1) / np.sqrt(runs)}
    return perf_stats

gamma = [0.99, 0.995, 1.0]
for g in gamma:
    print("Starting algorithm with gamma:", g)
    perf_stats += [run_algo(env, alpha, gamma=g, traj_len=traj_len)]

# And plot the results
for el in perf_stats:
    plt.plot(el['mean_rwd'], label='discount factor = ' + str(el['gamma']))
    plt.fill_between(np.arange(len(el['mean_rwd'])), el['mean_rwd'] + el['std_err'], el['mean_rwd'] - el['std_err'],
                     alpha=0.3)
plt.legend()
plt.xlabel('Iteration')
plt.ylabel('Returns')
plt.xlim([0, 200])
plt.show()

# Exercise I.3(b): Run the algo again, but with traj_len=500.
# Does the relative performance of learning using discount factors change?
```

Machine Learning Summer School 2019 (Moscow, Russia)/tutorials/reinforcement_learning3/part_i.ipynb


# Earth Launcher

*By Shashank Swaminathan and Timothy Novak*

The following code configures the notebook and imports relevant libraries

```python
# Configure Jupyter so figures appear in the notebook
%matplotlib inline

# Configure Jupyter to display the assigned value after an assignment
%config InteractiveShell.ast_node_interactivity='last_expr_or_assign'
# import functions from the modsim.py module
from modsim import *
import numpy as np
#imports image processing
from IPython.display import Image
```

Units are included for readability in some parts of the code. They were originally defined using the units library, but because solver functions got rid of units it became troublesome to ensure consistency throughout the code. We have redefined them as 1 to avoid conflicts and errors.

```python
m = 1 # UNITS.meter
s = 1 # UNITS.second
year = 1 # UNITS.year
kg = 1 # UNITS.kilogram
A = 1 # UNITS.ampere
V = 1 # UNITS.volts
Ohm = 1 # UNITS.ohm
N = 1 # UNITS.newton
U_mu = N / A**2
None
```

# Question

What are the design requirements for a railgun such that firing the railgun can accelerate the earth to the escape velocity of the sun?

The above modeling question might seem highly rediculous, hovever similar situations are being studied as spacecraft launch systems. One example is the electromagnetic mass driver which is a hypothetical concept of using a electromagnetically powered cannon, like a railgun or coilgun, to accelerate spacecraft for launch (see Applications section of the following page: https://en.wikipedia.org/wiki/Railgun or the dedicated page on the subject https://en.wikipedia.org/wiki/Mass_driver) This project models the reverse situation where the momentum gennerated by the shot is intended to accelerate the earth rather than to accelerate a projectile.

# Model
A schematic diagram of the model can be seen below.

```python
#prints an image with the IPython display library
Image("Diagram.png")
```

# Railgun Model

First steps first, we define important railgun parameters that we will use in the simulation:
 - Railgun length: 10 kilometers.
 - Starting position of the armature (projectile): 0 meters.
 - Diameter of the actual railgun: Half a meter.
 - Radius of the railgun rods: 0.05 meters.
 - Mass of the armature: 1 kilogram. (This is subject to change per simulation.)
 - Voltage Supply to the railgun: 1000 V. (This is subject to change per simulation.)
 - Resistance of the railgun circuit: 1e-10 Ohms (representing near super-conductivity).
 - Vacuum Permeability: An SI defined constant.
 - Simulation end time (if an event has not already stopped simulation): 1 year.

```python
params_railgun = Params(x = 0 * m,
                        d = 0.5 * m,
                        r = 0.05 * m,
                        L = 10e3 * m,
                        mass = 1 * kg,
                        Vs = 1000 * V,
                        R = 1e-10 * Ohm,
                        mu_0 = 1.257e-6 * U_mu,
                        t_end = 1 * year)
```

      values

      x
      0.000000e+00

      d
      5.000000e-01

      r
      5.000000e-02

      L
      1.000000e+04

      mass
      1.000000e+00

      Vs
      1.000000e+03

      R
      1.000000e-10

      mu_0
      1.257000e-06

      t_end
      1.000000e+00

The next step is to make a function that takes these parameters and returns a more optimized System object that will be used in simulation. The System object's contents are described in its docstring.

```python
def make_railgun_system(params):
    """
    Make a system object.

    Parameters:
        params: Params object containing following:
            x: Initial position of armature (in meters)
            d: Separation distance between two rails (in meters)
            r: Radii of the rails (in meters)
            L: Length armature has to travel along rails before launch (in meters)
            mass: Mass of armature (in kilograms)
            Vs: (Initial) Voltage supply magnitude (in volts)
            R: Resistance of railgun circuit (in ohms)
            mu_0: Vacuum permeability constant (in N/A^2)
            t_end: Simulation end time (in seconds)

    returns: System object containing
        params values
        c_mag: Calculated magnetic force coefficient (in N/A^2)
        init: Initial state object containing:
            x: Position of armature (initialized to params.x) (in meters)
            v: Velocity of armature (initialized to 0) (in meters)
    """

    # make the initial state
    init = State(x=params.x, v=0)

    # Determine magnetic force coefficient
    c_mag = params.mu_0 * log((params.d - params.r) / params.r) / (2 * pi)

    return System(params, c_mag = c_mag, init=init)
```

For simulating a railgun, the most important (and what we assume as the only active) force is the magnetic force formed from the rails and moving armature. This can be described via the equation

$
\begin{equation}
F_{mag} = \frac{\mu_0 * I ^2 * \log(\frac{d-r}{r})}{2\pi}
\end{equation}$

We simplify this by abstracting out the constants into a value called $c_{mag}$ (which was created in the **make_system** function). This leaves the following equation for magnetic force, which is used in the magnetic force function defined in the cell below:

$
\begin{equation}
    F_{mag} = c_{mag} * I^2
\end{equation}
$

```python
def mag_force(I, system):
    """
    Determines magnetic force on armature due to magnetic loop of railgun.
    Assumes 1D system.

    Parameters:
        I: Current through the circuit at time of function call. (in amperes)
        system: System object containing following system parameters (relevant listed):
            c_mag: Calculated magnetic force coefficient (in N/A^2)

    returns: Magnitude of magnetic force
    """
    return system.c_mag * I**2
```

To simulate this system we are going to use $modsim.py$'s wrapper function for **solve_ivp**, **run_ode_solver**, and so we now create a slope function that will wrap the magnetic force function from earlier and create derivatives for simulation:

```python
def railgun_slope_func(state, t, system):
    """
    Computes derivatives of the state variables.
    Invokes function mag_force to determine magnetic force applied on armature.

    Parameters:
        state: Current State object at time of function call.
            x: Position of armature (initialized to params.x) (in meters)
            v: Velocity of armature (initialized to 0) (in meters)
        system: System object containing following system parameters (relevant listed):
            Vs: Magnitude of voltage supply to circuit (in volts)
            R: Resistance of the circuit (in ohms)

    returns: Magnitude of magnetic force (in newtons)
    """
    x, v = state

    I = system.Vs / system.R

    F_mag = mag_force(I, system)
    a = F_mag / system.mass

    return v, a
```

Finally, we introduce an event that will halt the simulation once it occurs: the position of the armature reaching the end of the railgun (and therefore launching from the railgun).

```python
def event_func_railgun(state, t, system):
    """
    Stop when the position is equal to the length of rail that can be traveled.
        (stop when the distance left to travel is equal to 0)

    Parameters:
        state: Current State object at time of function call.
            x: Position of armature (initialized to params.x) (in meters)
            v: Velocity of armature (initialized to 0) (in meters)
        system: System object containing following system parameters (relevant listed):
            L: Length armature has to travel along rails before launch (in meters)

    returns: Distance the armature has left to travel.
    """
    x, v = state
    return x - system.L
```

We wrap the slope and event functions from earlier into another function, which we will name **sim_railgun**. This function will run the solver function around the **railgun_slope_function**, and will halt the simulation once the event defined in the **event_func_railgun** occurs. The returned values from this simulation function will the the position and velocity of the armature over time; these will then be used by the functions defined in the following section, the ones which describe the motion of the planetery bodies in both stable and perturbed orbits.

```python
def sim_railgun(mass, v_supply, params, max_step = 0):
    """
    Simulate the railgun model.

    Parameters:
        mass: Mass of armature (in kilograms)
        params: Params object containing following:
            x: Initial position of armature (in meters)
            d: Separation distance between two rails (in meters)
            r: Radii of the rails (in meters)
            L: Length armature has to travel along rails before launch (in meters)
            mass: Mass of armature (in kilograms)
            Vs: (Initial) Voltage supply magnitude (in volts)
            R: Resistance of railgun circuit (in ohms)
            mu_0: Vacuum permeability constant (in N/A^2)
            t_end: Simulation end time (in seconds)

    returns:
        x: Final position of the armature.
        v: Final velocity of the armature.
    """
    params = Params(params, mass=mass, Vs = v_supply)
    system = make_railgun_system(params)
    if max_step == 0:
        results, details = run_ode_solver(system,
                                          railgun_slope_func,
                                          events=event_func_railgun)
    else:
        results, details = run_ode_solver(system,
                                          railgun_slope_func,
                                          events=event_func_railgun,
                                          max_step = 1e-16)
    return results.x, results.v
```

# Planetary Motion Model code
The following functions describe the planitary motion simulation

The following code describes the main function which is used in running simulations of a planet in orbit around the sun. This function returns a time frame detailing the velocity and position of the plenet at varous points in time.

```python
def run_planet_simulation (earth_position_x,
                           earth_position_y,
                           earth_velocity_x,
                           earth_velocity_y,
                           earth_mass,num_years):
    """This function runs a simulation of a two body planetary system.
    This simulation assumes:
        The sun in the planetary system has the mass of earth's sun,
        and is at a fixed location in space (0,0) for convenience.
    The planet's position velocity and mass are supplied,
        to better inform the simulation.
    The number of years dictates how many years the simulation should be run for.
    Max_step_size is the maximum step size in seconds.
    The function returns the trajectory of the supplied planet."""
    # creates the state for the simulation
    #distance in meteres velocity in m/s
    st8 = State(xpos = earth_position_x,
                ypos = earth_position_y,
                xvel = earth_velocity_x,
                yvel = earth_velocity_y)
    #Creates a system which holds the parameters of the simulation
    #G = m^3/(kg*s^2)
        # masses are in kg
        # radi are in meters
        # velocities are in m/s
    sys = System(init = st8,
                 G = 6.67408*10**(-11) * (m**3)/(kg*s**2),
                 M_earth = earth_mass,
                 M_sun =1.989*10**30*kg,
                 R_earth = 6.3781*10**6*m,
                 R_sun= 6.95700*10**8*m,
                 t_end = num_years*s/year,#60*60*24*365.25
                 Earth_Radius = 6378*10**3*m,
                 sun_pos = Vector(0*m,0*m))

    #runs an ODE solver to run the simulation -
        # restricts the ODE solver to taking steps with a maximum size of one day
#     ts = linrange(0, 32*10**6, 1000)
    results,details = run_ode_solver(sys,planet_slope_func, max_step=60*60*24*s)
    results.xpos *= m
    results.ypos *= m
    results.xvel *= m/s
    results.yvel *= m/s

    #returns the results of the solver
    return results
```

The following function  performs the momentum conservation calculations on the earth. This function returns an updated mass and velocity of the earth based on the change in momentum from the firing of a projecile with a given mass and velocity.

```python
def fire_cannon(planet_velocity_vector,
                Mass_earth,
                Mass_shot,
                Shot_speed):
    """
    Runs a function which:
        updates the velocity and mass of a planet
        Assumes it is accelerated via mass ejection.
    Calculations are based on conservation of momentum.
    Masses are in kilograms
    Speeds are in m/s
    The velocity vector describes the last known velocity of the planet.
    MODELING ASSUMPTION CONSTRAINT:
        The model assumes the earth exerts no gravitational pull on the projectile.
        Implies projectiles fired at less than escape velocity will not be accurate
            Does not account for where they would fall back to the earth."""
    if Shot_speed != 0*m/s:
        #calculates the magnitude of the new velocity of the planet
        velocity_vector_magnitude = (Mass_earth*planet_velocity_vector.mag + \
                                     Mass_shot*Shot_speed)/(Mass_earth-Mass_shot)

        #This creates a velocity vector in the direction of the previous velocity
        planet_vel_unit_vector = planet_velocity_vector/planet_velocity_vector.mag
        #multiplies magnitude by the unit vector to get a vector in the proper direction with the proper magnitude
        planet_velocity_vector = planet_vel_unit_vector * velocity_vector_magnitude
        #updates the mass of the earth to account for shot's mass leaving earth
        Mass_earth = Mass_earth - Mass_shot

    #retuns the new velocity as a vector and the new mass of the earth
    return planet_velocity_vector, Mass_earth
```

The remaining functions expand upon the above functions in more detail. The in depth explanation can be skipped by proceding to the **Combined Launch Model Code** section below.

```python
def planet_slope_func(st8, t, sys):
    """
    Describes the change in:
        position and velocity in both the x and y plane.
    The function takes in a state which describes the old position of the system
    t is the time of the syatem -
        it is not actually used in calculations
        it is a required argument for use with an ODE Solver.
    sys is a system object which contains system constants
    """

    #extracts the values from the state
    #This is as the ODE solver does not store variables in a state object
    xpos,ypos,xvel,yvel= st8
    #creates a vector which describes the position of the planet
    planet_pos = Vector(xpos,ypos)

    #describes a system of differential equations
    #update the velocity in each direction
    dxveldt = get_grav_acceleration(sys.G, sys.M_sun, sys.M_earth,
                                    planet_pos,sys.sun_pos).x
    dyveldt = get_grav_acceleration(sys.G, sys.M_sun, sys.M_earth,
                                    planet_pos,sys.sun_pos).y

    #update the position in each direction
    dxposdt = xvel
    dyposdt = yvel

    #returns the change in the state variables
    return dxposdt,dyposdt,dxveldt,dyveldt
```

This function finds the magnitude of the gravitational acceleration on a planet by the sun using F = ma. The function returns the acceleration in terms of m/s^2.

```python
def get_grav_acceleration(G, M1, m2, planet_vector, sun_vector):
    """
    Calculates acceleration due to gravity with F = ma
    F in N
    masses in kg
    a in m/s^2
    """
    #gets the force & divides by the orbiting body to finding orbital acceleration
    return get_gravitational_force(G,M1,m2,planet_vector,sun_vector)/m2
```

The following function applies the law of universal gravitation to find the magnitude of the gravitational force for a planet orbiting a star. The function retunrs a force in N which describes the gravitational force.

```python
def get_gravitational_force(G,M1,m2,planet_vector, sun_vector):
    """
    Applies law of universal gravitation
    Returns the force of gravity as a vector towards the center of the circle
    in N
    """
    #calculates the vector pointing from the planet to the sun
    planet_to_sun_vector = sun_vector-planet_vector

    #takes the direction of the previous vector
    Direction_vector = planet_to_sun_vector/planet_to_sun_vector.mag

    #calculates the acceleration of the planet
    acceleration_mag = (G*M1*m2)/(planet_vector.mag**2)

    #Returns a vector which has:
        # Magnitude of the acceleration
        # Direction towards the star
    return Direction_vector * acceleration_mag
```

# Combined Launch Model Code

The following function runs a simulation of firing the railgun and the effect which this action has on the orbit of the earth. The function returns a time frame describing the earth's position and velocity at various points in time after firing the railgun.

```python
def launch_simulation(mass_armature,
                      railgun_voltage,
                      railgun_params,
                      orbital_results):
    """This function runs a simulation of the launch of a projectile """

    # Simulate railgun launch
    rail_x, rail_v = sim_railgun(mass_armature * kg,
                                 railgun_voltage,
                                 railgun_params)

    final_armature_speed = get_last_value(rail_v)

    #gets a vector of the velocity of earth
    #This is based on the results of the original simulation
    orbital_velocity = Vector(get_last_value(orbital_results.xvel*m),
                              get_last_value(orbital_results.yvel*m))

    #fires the railgun
    post_cannon_earth_velocity, post_cannon_earth_mass = fire_cannon(orbital_velocity,
                                                         5.972*10**24*kg,
                                                         mass_armature,
                                                         final_armature_speed)
    return post_cannon_earth_velocity, post_cannon_earth_mass
```

# Optimization

This code takes a voltage supply value and a set of parameters, and calculates the final velocity of the planetary body that has had a projectile launched from it using a railgun (described within the parameter set). It relies on the functions defined prior to this cell. Its purpose is to allow ease of optimization (using the $fsolve$ function) of the final velocity of the launched planetary body, based on voltage supply input to the railgun.

```python
def voltage_optimizer(Vs, sim_params):
    """
    Used to find minimum voltage required to achieve escape velocity.

    Parameters:
        Vs: Voltage supply to the railgun
            To be optimized using fsolve.
        sim_params: Additional parameters ued in simulation (held constrant)
            mass: Mass of armature shooting from Earth.
            railgun: Railgun parameters
            orbital_results: Current state of a stable Earth in orbit.

    Returns:
        The difference of Earth's velocity post railgun launch,
        and the theoretical escape velocity value.
    """
    res_v, res_m = launch_simulation(sim_params.mass,
                              Vs,
                              sim_params.railgun,
                              sim_params.orbital_results)
    return res_v.mag - sim_params.v_escape
```

To do optimizing of the mass based on voltage supply, we do the same as the function above once again, this time changing the optimized variable from voltage supply to mass.

```python
def mass_optimizer(mass, sim_params):
    """
    Used to find minimum voltage required to achieve escape velocity.

    Parameters:
        mass: Mass of armature shooting from Earth.
              To be optimized using fsolve.
        sim_params: Additional parameters ued in simulation (held constrant)
            Vs: Voltage supply to the railgun
            railgun: Railgun parameters
            orbital_results: Current state of a stable Earth in orbit.

    Returns:
        The difference of Earth's velocity post railgun launch,
        and the theoretical escape velocity value.
    """
    res_v, res_m = launch_simulation(mass,
                              sim_params.Vs,
                              sim_params.railgun,
                              sim_params.orbital_results)
    return res_v.mag - sim_params.v_escape
```

# Results

The following code describes the inital state of the earth. values have been chosen to place the earth at a distance of one astronomical unit (AU) away from the sun (the average distance from the earth to the sun), moving at a velocity which corrasponds to the observed velocity withwhich the earth orbits the sun (assuming a nearly circular orbit and a distance from the sun of 1 AU), and the measured mass of the earth.
The simulation is set to run for one year to demonstrate earth in stable orbit before the railgun is fired.

```python
#Describes the inital position, in vector format, of the earth (m)
inital_earth_x_position = 149597900000*m
inital_earth_y_position = 0*m

#Describes the inital velocity, in vector format, of the earth (m/s)
inital_earth_x_velocity = 0*m/s
inital_earth_y_velocity = 29785*m/s

#Describes the inital mass of the earth (kg)
inital_earth_mass = 5.972*10**24*kg

#Describes the length of the stable orbit simulation (years)
stable_orbit_simulation_runtime = 365.25*60*60*24*s
None
```

```python
orbital_results = run_planet_simulation(inital_earth_x_position,
                                        inital_earth_y_position,
                                        inital_earth_x_velocity,
                                        inital_earth_y_velocity,
                                        inital_earth_mass,
                                        stable_orbit_simulation_runtime)

```

      xpos
      ypos
      xvel
      yvel

      0.000000e+00
      1.495979e+11
      0.000000e+00
      0.000000e+00
      29785.000000

      4.748073e-08
      1.495979e+11
      1.414214e-03
      -2.816389e-10
      29785.000000

      5.222880e-07
      1.495979e+11
      1.555635e-02
      -3.098027e-09
      29785.000000

      5.270361e-06
      1.495979e+11
      1.569777e-01
      -3.126191e-08
      29785.000000

      5.275109e-05
      1.495979e+11
      1.571191e+00
      -3.129008e-07
      29785.000000

      5.275584e-04
      1.495979e+11
      1.571333e+01
      -3.129289e-06
      29785.000000

      5.275631e-03
      1.495979e+11
      1.571347e+02
      -3.129318e-05
      29785.000000

      5.275636e-02
      1.495979e+11
      1.571348e+03
      -3.129320e-04
      29785.000000

      5.275637e-01
      1.495979e+11
      1.571348e+04
      -3.129321e-03
      29785.000000

      5.275637e+00
      1.495979e+11
      1.571348e+05
      -3.129321e-02
      29785.000000

      5.275637e+01
      1.495979e+11
      1.571348e+06
      -3.129321e-01
      29784.999998

      5.275637e+02
      1.495979e+11
      1.571348e+07
      -3.129321e+00
      29784.999836

      5.275637e+03
      1.495978e+11
      1.571348e+08
      -3.129320e+01
      29784.983565

      5.275637e+04
      1.495896e+11
      1.571319e+09
      -3.129263e+02
      29783.356525

      1.391564e+05
      1.495405e+11
      4.144242e+09
      -8.253207e+02
      29773.566064

      2.255564e+05
      1.494470e+11
      6.715938e+09
      -1.337471e+03
      29754.963132

      3.119564e+05
      1.493094e+11
      9.285646e+09
      -1.849226e+03
      29727.553223

      3.983564e+05
      1.491275e+11
      1.185261e+10
      -2.360433e+03
      29691.344432

      4.847564e+05
      1.489015e+11
      1.441606e+10
      -2.870943e+03
      29646.347453

      5.711564e+05
      1.486314e+11
      1.697524e+10
      -3.380604e+03
      29592.575575

      6.575564e+05
      1.483174e+11
      1.952940e+10
      -3.889264e+03
      29530.044679

      7.439564e+05
      1.479594e+11
      2.207778e+10
      -4.396774e+03
      29458.773233

      8.303564e+05
      1.475576e+11
      2.461963e+10
      -4.902984e+03
      29378.782288

      9.167564e+05
      1.471122e+11
      2.715418e+10
      -5.407744e+03
      29290.095467

      1.003156e+06
      1.466232e+11
      2.968071e+10
      -5.910904e+03
      29192.738965

      1.089556e+06
      1.460909e+11
      3.219844e+10
      -6.412316e+03
      29086.741535

      1.175956e+06
      1.455152e+11
      3.470665e+10
      -6.911831e+03
      28972.134486

      1.262356e+06
      1.448965e+11
      3.720458e+10
      -7.409302e+03
      28848.951666

      1.348756e+06
      1.442350e+11
      3.969150e+10
      -7.904581e+03
      28717.229460

      1.435156e+06
      1.435307e+11
      4.216667e+10
      -8.397523e+03
      28577.006772

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

      2.908316e+07
      1.320142e+11
      -7.035987e+10
      1.401248e+04
      26283.973722

      2.916956e+07
      1.332053e+11
      -6.807863e+10
      1.355814e+04
      26521.131293

      2.925596e+07
      1.343569e+11
      -6.577725e+10
      1.309978e+04
      26750.437108

      2.934236e+07
      1.354688e+11
      -6.345639e+10
      1.263755e+04
      26971.823423

      2.942876e+07
      1.365406e+11
      -6.111674e+10
      1.217158e+04
      27185.224836

      2.951516e+07
      1.375719e+11
      -5.875901e+10
      1.170201e+04
      27390.578307

      2.960156e+07
      1.385626e+11
      -5.638389e+10
      1.122898e+04
      27587.823171

      2.968796e+07
      1.395122e+11
      -5.399207e+10
      1.075263e+04
      27776.901162

      2.977436e+07
      1.404205e+11
      -5.158427e+10
      1.027310e+04
      27957.756426

      2.986076e+07
      1.412873e+11
      -4.916121e+10
      9.790526e+03
      28130.335542

      2.994716e+07
      1.421123e+11
      -4.672359e+10
      9.305058e+03
      28294.587531

      3.003356e+07
      1.428951e+11
      -4.427215e+10
      8.816839e+03
      28450.463877

      3.011996e+07
      1.436357e+11
      -4.180759e+10
      8.326011e+03
      28597.918536

      3.020636e+07
      1.443338e+11
      -3.933067e+10
      7.832721e+03
      28736.907956

      3.029276e+07
      1.449892e+11
      -3.684210e+10
      7.337114e+03
      28867.391082

      3.037916e+07
      1.456016e+11
      -3.434263e+10
      6.839336e+03
      28989.329376

      3.046556e+07
      1.461709e+11
      -3.183299e+10
      6.339536e+03
      29102.686820

      3.055196e+07
      1.466970e+11
      -2.931393e+10
      5.837861e+03
      29207.429935

      3.063836e+07
      1.471797e+11
      -2.678620e+10
      5.334459e+03
      29303.527784

      3.072476e+07
      1.476188e+11
      -2.425053e+10
      4.829479e+03
      29390.951984

      3.081116e+07
      1.480142e+11
      -2.170769e+10
      4.323071e+03
      29469.676715

      3.089756e+07
      1.483658e+11
      -1.915843e+10
      3.815384e+03
      29539.678725

      3.098396e+07
      1.486734e+11
      -1.660349e+10
      3.306569e+03
      29600.937340

      3.107036e+07
      1.489371e+11
      -1.404364e+10
      2.796775e+03
      29653.434468

      3.115676e+07
      1.491567e+11
      -1.147963e+10
      2.286155e+03
      29697.154604

      3.124316e+07
      1.493321e+11
      -8.912220e+09
      1.774858e+03
      29732.084836

      3.132956e+07
      1.494634e+11
      -6.342176e+09
      1.263037e+03
      29758.214847

      3.141596e+07
      1.495504e+11
      -3.770254e+09
      7.508415e+02
      29775.536921

      3.150236e+07
      1.495931e+11
      -1.197217e+09
      2.384243e+02
      29784.045942

      3.155760e+07
      1.495972e+11
      4.482013e+08
      -8.925872e+01
      29784.866288

379 rows × 4 columns

The following code runs a simulation which sweeps acoss masses of armatures (projectiles) ranging from $10^9$ kg (about the mass of the heaviest crane vessel) to $10^{24}$ kg (roughly one fifth the mass of the earth). The point of this is to find the minimum voltage that must be supplied to the railgun, per every sweeped mass value, to get the Earth into escape velocity.

The mass sweep starts at $10^9$ kg because at every mass smaller than that, the voltage goes higher than what can be simulated feasibly. This is because at voltages higher than approximately $10^{20}$ V, the projectile launches of the railgun (of length $10$ km) in less than $10^{-15}$ seconds. Thus, to be able to accurately capture the event, the simulation step size must be smaller than $10^{-17}$ s, which slows downs simulations incredibly. This also makes actual optimization operations (which involve multiple simulations) nearly impossible to conduct - one such attempt took more than a day to complete.

Here, we define important constants that we will use in the sweep; namely, the gravitational constant G, the mass of the Sun, the distance between the sun and the earth, and the escape velocity from the Sun.

```python
G_space = 6.67408e-11 * m ** 3 / (kg * s ** 2)
M_sun =1.989e30 * kg
R_s_e = 1.496e11 * m
v_escape = np.sqrt(2 * G_space * M_sun / R_s_e)
None
```

Now, we encapsulate these values into a Params object, along with the previously defined railgun parameters, and the stable orbit results calculated eaerlier.

```python
sim_params = Params(railgun = params_railgun,
                    orbital_results = orbital_results,
                    v_escape = v_escape,
                    mass = 1e24,
                    Vs = 30e12 * V)
```

      values

      railgun
      x        0.000000e+00
d        5.000000e-01
r ...

      orbital_results
      xpos          ypos      ...

      v_escape
      42127.2

      mass
      1e+24

      Vs
      3e+13

Finally, we define the masses over which we would like to sweep. To make the optimization process more accurate, we also provide a list of initial guesses of potential optimal voltages to correspond with each armature mass value. An empty Series object to hold the results of the optimization is also created.

```python
possible_masses = np.logspace(2, 24, 100)[32:99]
vs_opt_guesses = pd.Series(np.append(np.logspace(20, 10, 40)[32:39],
                                     np.ones(100-40) * 1e10),
                           index = possible_masses)
fin_vs = pd.Series(index = possible_masses)
None
```

We use a combination of $fsolve$ and the $voltage$ $optimizer$ functions to find the minimum voltage required to reach escape velocity, for each sweeped mass.

```python
for mass in possible_masses:
    sim_params = Params(sim_params, mass = mass)
    v_s_opt = fsolve(voltage_optimizer,
                     vs_opt_guesses[mass] * V,
                     sim_params)
    fin_vs[mass] = v_s_opt
```

```python
possible_voltages = np.geomspace(1e3, fin_vs[fin_vs.index[-1]], 100)
fin_m = pd.Series(index = possible_voltages)
None
```

```python
for voltage in possible_voltages:
    sim_params = Params(sim_params, Vs = voltage)
    m_opt = fsolve(mass_optimizer,
                   1e23,
                   sim_params)
    fin_m[voltage] = m_opt
```

To give the results some context, here we define some masses and voltages corresponding to the real world:
 - Great Wall of China: This is the heaviest structure built by man so far.
 - A fifth of the Earth's Mass - the description is in the name.
 - Planck's Voltage - This is the highest theoretically achievable voltage. The value being shown here is Planck's Voltage times $10^{-10}$.
 - Maximum Recorded Voltage - We have two values for these:
   1. Maximum Voltage outside a linear accelerator: This was generated in Oak Ridge National Labs using a Van de Graaf.
   1. Maximum Voltage produced ever by humans: This was produced using lasers within one millimeter of each other in a linear accelerator.

```python
conc_vs = np.append(fin_m.index, np.flip(fin_vs.values))
conc_m = np.append(fin_vs.index, fin_m.values)
len_fin = len(conc_vs)

great_wall = pd.Series(conc_vs, np.ones(len_fin) * 5.262e10)
mass_earth_fifth = pd.Series(conc_vs,
                                     np.ones(len_fin) * 1.19e24)
planck_voltage = pd.Series(np.ones(len_fin) * 1.042e17,
                                     conc_m)
max_rec_voltage = pd.Series(np.ones(len_fin) * 25.5e6,
                                     conc_m)
max_l_rec_voltage = pd.Series(np.ones(len_fin) * 2e11,
                                     conc_m)
None

```

Here, we plot the optimization results, along with some other physical values to give context.

```python
plt.figure(1)
plt.figure(figsize=[14,11])
plt.loglog(fin_vs.index, fin_vs.values,
           fin_m.values, fin_m.index,
           great_wall.index, great_wall.values,
           mass_earth_fifth.index, mass_earth_fifth.values,
           planck_voltage.values, planck_voltage.index,
           max_l_rec_voltage.index, max_l_rec_voltage.values)
plt.xlabel('Mass of Armature Shooting from the Earth')
plt.ylabel('Voltage Supply Magnitude')
plt.title('Armature Mass vs. Voltage Supply to Achieve Escape Velocity')
plt.legend(['Minimum voltage for given mass to acheive escape velocity',
            'Mass of Great Wall of China',
            'A fifth of the mass of the Earth',
            'Planck\'s Voltage, times 10^-10',
            'Largest recorded voltage (using lasers in a linear accelerator)'])
```

```python
fin_m
```

    1.000000e+03    1.749641e+24
    1.118746e+03    1.749641e+24
    1.251592e+03    1.749641e+24
    1.400213e+03    1.749641e+24
    1.566482e+03    1.749641e+24
    1.752494e+03    1.749641e+24
    1.960595e+03    1.749641e+24
    2.193407e+03    1.749641e+24
    2.453864e+03    1.749641e+24
    2.745250e+03    1.749641e+24
    3.071236e+03    1.749641e+24
    3.435932e+03    1.749641e+24
    3.843933e+03    1.749641e+24
    4.300383e+03    1.749641e+24
    4.811035e+03    1.749641e+24
    5.382324e+03    1.749641e+24
    6.021451e+03    1.749641e+24
    6.736471e+03    1.749641e+24
    7.536397e+03    1.749641e+24
    8.431310e+03    1.749641e+24
    9.432491e+03    1.749641e+24
    1.055256e+04    1.749641e+24
    1.180563e+04    1.749641e+24
    1.320749e+04    1.749641e+24
    1.477582e+04    1.749641e+24
    1.653039e+04    1.749641e+24
    1.849330e+04    1.749641e+24
    2.068929e+04    1.749641e+24
    2.314605e+04    1.749641e+24
    2.589454e+04    1.749641e+24
                        ...
    2.577463e+06    1.742710e+24
    2.883525e+06    1.740966e+24
    3.225931e+06    1.738783e+24
    3.608996e+06    1.736051e+24
    4.037548e+06    1.732632e+24
    4.516989e+06    1.728352e+24
    5.053361e+06    1.722996e+24
    5.653425e+06    1.716292e+24
    6.324744e+06    1.707901e+24
    7.075779e+06    1.697400e+24
    7.915997e+06    1.684256e+24
    8.855986e+06    1.667806e+24
    9.907595e+06    1.647217e+24
    1.108408e+07    1.621448e+24
    1.240026e+07    1.589195e+24
    1.387274e+07    1.548828e+24
    1.552006e+07    1.498306e+24
    1.736300e+07    1.435072e+24
    1.942478e+07    1.355928e+24
    2.173139e+07    1.256873e+24
    2.431189e+07    1.165474e+24
    2.719882e+07    1.111437e+24
    3.042856e+07    1.054260e+24
    3.404182e+07    9.941793e+23
    3.808413e+07    9.315331e+23
    4.260646e+07    8.667734e+23
    4.766578e+07    8.004670e+23
    5.332588e+07    7.332905e+23
    5.965809e+07    6.660158e+23
    6.674223e+07    5.994843e+23
    Length: 100, dtype: float64

*Figure 1*

```python
params_railgun = Params(params_railgun, mass = 1.8*10**24*kg, Vs = 1*10**3*V)
post_cannon_earth_velocity,post_cannon_earth_mass = launch_simulation(params_railgun.mass, params_railgun.Vs, params_railgun, orbital_results)
post_cannon_results = run_planet_simulation(get_last_value(orbital_results.xpos),get_last_value(orbital_results.ypos),post_cannon_earth_velocity.x,post_cannon_earth_velocity.y, post_cannon_earth_mass,1)

```

      xpos
      ypos
      xvel
      yvel

      0.000000
      1.495972e+11
      4.482013e+08
      -127.769198
      42635.479750

      0.716586
      1.495972e+11
      4.482318e+08
      -127.773448
      42635.479737

      1.000000
      1.495972e+11
      4.482439e+08
      -127.775129
      42635.479732

```python
plt.figure(2)
plt.figure(figsize=[14,11])
plt.plot(orbital_results.xpos, orbital_results.ypos,post_cannon_results.xpos, post_cannon_results.ypos)
plt.xlabel('Distance from the sun (m)')
plt.ylabel('Distance from the sun (m)')
plt.title('Trajectory of the Earth in Space')
plt.legend(['Stable Orbit Trajectory',
            'Trajectory with a shot of mass () and',
            'A fifth of the mass of the Earth',
            'Planck\'s Voltage, times 10^-10',
            'Largest recorded voltage (using lasers in a linear accelerator)'])
```

# Interpretation:

As can be seen from the figure describing voltage and mass curves, the design requirements for a railgun which could accelerate the earth to escape velocity are quite significant. If smaller projectile masses were to be used, where 'smaller' is a relative term corelating to projectiles on the order of some of the largest man made objects, extremely high voltages would be required. For example, firing a mass the size of the great wall of china (indicated by the line in Figure 1) is simply impossible because it would require voltages higher than the highest recorded voltage on Earth. Althouh these voltages are possible, they can't be sustained for the time period which would be necessary for launching the projectile.
The use of smaller voltages is still problematic. Dropping the voltage down to standard operating voltages ($\approx3,000 V$) necessitates a mass of $1.75*10^{24} kg$. Comparing this amount to the mass extracted from the largest excavation humanity has accomplished. The largest excavation is the Bingham Canyon Mine, an open pit mine in utah (https://en.wikipedia.org/wiki/Bingham_Canyon_Mine), which coveres an area of $7,689,034 meteres^2$ and has a depth of $950 meteres$. The mass of the dirt removed can be approximated by finding the volume as approximated by a cone ($2,486,120,993 kg$), and multiplying by the average density of the soil (2,900 $kg/m^3$) (https://en.wikipedia.org/wiki/Lithosphere) which equates to about $7,209,750,880,667 kg$.This means that to create the projectile simulated in the orbital simulation above we would need to excavate $249,661,885,659$ Bingham Mines. or the equivalency of
excavating the entire earth's surface to a depth of $3,804 km$. Obviously seeing as this far outstrips humanity's proven ability creating such a railgun to accelerate the earth would be impossible.

The specifications above demonstrate


First, load an instance of the CADRE model and render its depgraph:

```
from IPython.display import SVG, Image, HTML
from CADRE import CADRE
import networkx as nx
assembly = CADRE(150, 100)
graph = assembly._depgraph.component_graph()

defaults = ["derivative_exec_count", "directory", "itername", "exec_count",
            "force_execute", "driver"]
remove = []
for node in graph.nodes_iter():
    for d in defaults:
        if d in node:
            remove.append(node)
            break

for node in remove:
    graph.remove_node(node)

ag = nx.to_agraph(graph)
ag.layout('dot')
ag.draw('design.svg')
HTML('')
```

Load an optimized CADRE state, and define a few functions for post processing the results

```
import pickle
import pygmaps
data = pickle.load(open("src/CADRE/test/data1346.pkl"))

savedir = "docs/maps"

def val2hex(v, scale=1.0):
    if not v:
        return "#000044"
    j = pylab.get_cmap("jet")
    v = v / scale
    nums = [int(255 * i) for i in j(v)][:3]
    return ''.join(["00" if i == 0 else hex(i)[2:] for i in nums])

def calc_lat_lon(r_e2b_I, O_IE):
    r2d = 180 / np.pi
    rgs = 6378.137
    lats, lons = [], []
    n = r_e2b_I.shape[1]
    for i in xrange(n):

        r_e2g_I = r_e2b_I[:3, i]
        r_e2g_I = r_e2g_I / np.linalg.norm(r_e2g_I) * rgs
        r_e2g_E = np.dot(O_IE[:, :, i].T, r_e2g_I)

        lat = np.arcsin(r_e2g_E[2] / rgs) * r2d
        lon = np.arctan2(r_e2g_E[1], r_e2g_E[0]) * r2d

        lats.append(lat), lons.append(lon)

    return lats, lons
```

Now, produce figures and maps for each design point

```
mxdata = max([max(data["%s:Dr" % str(i)]) for i in xrange(6)])

gmap_all = pygmaps.gmap(41, -88, 2)

for i in xrange(6):
    si = str(i)
    dr = data[si + ":Dr"]
    p = data[si + ":P_comm"]
    g = data[si + ":gamma"]
    s = data[si + ":SOC"]

    O_IE = data["%s:O_IE" % si]
    gmap = pygmaps.gmap(41, -88, 2)
    r_e2b_I = data["%s:r_e2b_I" % si]
    lats, lons = calc_lat_lon(r_e2b_I, O_IE)
    path = zip(lats, lons)
    gmap.add_weighted_path(path, dr, scale=mxdata)
    gmap_all.add_weighted_path(path, dr, scale=mxdata)
    gmap.draw(savedir + "/" + si + '_data.html')

    figure()
    suptitle("CADRE Design Point " + si)
    subplot(411)
    title("Dr")
    plot(dr)
    gca().get_xaxis().set_visible(False)

    subplot(412)
    title("P_comm")
    plot(p)
    gca().get_xaxis().set_visible(False)

    subplot(413)
    title("gamma")
    plot(g)
    gca().get_xaxis().set_visible(False)

    subplot(414)
    title("SOC")
    plot(s[0])
gmap_all.draw(savedir + '/all_data.html')
```

```
from IPython.display import display
from sympy.interactive import printing
printing.init_printing(use_latex=True)
import sympy
c,gr,l,f,k,t,s,p,gt,S,los = sympy.symbols("c G_r L_{l} f k T_s S_{NR} P_{comm} G_t S LOS_c")

B_r = (c**2 * gr * l) / (16*sympy.pi**2*f**2 *k *t*(s) ) * (p * gt)/(S**2) * los
```

Testing some symbolic stuff using the comm bitrate component:

$$B_r = \frac{ c^2 G_r L_l}{16 \pi^2 f^2 k T_s \left(SNR\right)} \frac{P_{comm}G_t}{S^2} LOS_c$$

$$\frac{\partial B_r}{\partial S} = $$

```
sympy.diff(B_r, S)
```

$$\frac{\partial B_r}{\partial P_{comm}} = $$

```
sympy.diff(B_r, p)
```

```
HTML('')
```

```

```