```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("data_sim.mat")
z_exp = np.squeeze(raw_data["z"])

```

```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.1
ld = 21e-9
lb = 570e-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 = c
    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) $")
```

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

     final_simplex: (array([[-0.0005],
           [-0.0005]]), array([38.3188893, 38.3188893]))
               fun: 38.31888929542506
           message: 'Optimization terminated successfully.'
              nfev: 160
               nit: 55
            status: 0
           success: True
                 x: array([-0.0005])

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

```python
offset = 0
z = np.squeeze(raw_data["z"])
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 = np.squeeze(raw_data["z"])
z_drifts = z_drifts
```

```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.5e-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
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 = np.squeeze(raw_data["z"])

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)

```

    C:\Users\m.lavaud\.conda\envs\analyse\lib\site-packages\numpy\lib\histograms.py:905: RuntimeWarning: invalid value encountered in true_divide
      return n/db/n.sum(), bin_edges

# Froce error

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

```python
def random_on_force():
    z = np.squeeze(raw_data["z"])*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)
```

    43

```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[15:]  * 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((-20, 120))
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
dict_force["z_hist"] = z_hist
dict_force["F_eq"] = F_eq

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

```


Stability of defection, optimisation of strategies and the limits of memory in the PD.
----------------------

## 2. Stability of defection

```python
import opt_mo

import numpy as np
import sympy as sym
import itertools
import axelrod as axl
import matplotlib.pyplot as plt
```

```python
from fractions import Fraction
```

```python
from axelrod.action import Action

C, D = Action.C, Action.D
```

```python
import tqdm
```

```python
import matplotlib.transforms as transforms
import matplotlib
```

```python
import warnings; warnings.simplefilter('ignore')
```

```python
import operator
import functools
```

```python
sym.init_printing()
```

```python
font = {"size": 10, "weight": "bold"}
matplotlib.rc("font", **font)
```

Analytical check
----------------

```python
p_1, p_2, p_3, p_4 = sym.symbols("p_1, p_2, p_3, p_4")
q_1, q_2, q_3, q_4 = sym.symbols("q_1, q_2, q_3, q_4")
k_1, k_2, k_3, k_4 = sym.symbols("k_1, k_2, k_3, k_4")

p, q, k = (p_1, p_2, p_3, p_4), (q_1, q_2, q_3, q_4), (k_1, k_2, k_3, k_4)
```

```python
def get_Q_N_derivative(player, opponent):
    x = np.array(player)

    Q = opt_mo.utility.quadratic_term_numerator(opponent)
    c = opt_mo.utility.linear_term_numerator(opponent)

    return np.dot(x, Q) + c
```

```python
def get_Q_N(player, opponent):
    x = np.array(player)

    Q = opt_mo.utility.quadratic_term_numerator(opponent)
    c = opt_mo.utility.linear_term_numerator(opponent)
    a = opt_mo.utility.constant_term_numerator(opponent)

    return np.dot(x, Q.dot(x.T) * 1 / 2) + np.dot(c, x.T) + a
```

```python
def get_Q_D(player, opponent):
    x = np.array(player)

    Q_bar = opt_mo.utility.quadratic_term_denominator(opponent)
    c_bar = opt_mo.utility.linear_term_denominator(opponent)
    a_bar = opt_mo.utility.constant_term_denominator(opponent)

    return np.dot(x, Q_bar.dot(x.T) * 1 / 2) + np.dot(c_bar, x.T) + a_bar
```

```python
def get_Q_D_derivative(player, opponent):
    x = np.array(player)

    Q_bar = opt_mo.utility.quadratic_term_denominator(opponent)
    c_bar = opt_mo.utility.linear_term_denominator(opponent)
    a_bar = opt_mo.utility.constant_term_denominator(opponent)

    return np.dot(x, Q_bar) + c_bar
```

**Check quadratic derivative of**

$$\frac{1}{2}pQp^T + cp +a $$

```python
expr = get_Q_N(p, q)
```

```python
diff = [sym.diff(expr, i) for i in p]
```

```python
derivatives = get_Q_N_derivative(p, q)
```

```python
for i in range(4):
    assert (diff[i] - derivatives[i]).simplify() == 0
```

**Check derivative of utility**

```python
def get_written_derivative_of_utility(player, opponents):
    sums = 0
    for opponent in opponents:
        numerator = (get_Q_N_derivative(player, opponent) * get_Q_D(player, opponent) -
                      get_Q_D_derivative(player, opponent) * get_Q_N(player, opponent))

        denominator = get_Q_D(player, opponent) ** 2

        sums += numerator / denominator
    return (sums) * (1 / len(opponents))
```

```python
for seed in range(100):
    num_players = 5
    np.random.seed(seed)
    opponents = [[np.random.random() for _ in range(4)] for _ in range(num_players)]

    np.random.seed(seed + 1000)
    player = [np.random.random() for _ in range(4)]

    written_derivative = get_written_derivative_of_utility(player, opponents)

    utility = opt_mo.tournament_utility(p, opponents)
    utility_derivative = [sym.diff(utility, i) for i in p]
    utility_derivative = [expr.subs({p_1: player[0], p_2: player[1], p_3: player[2], p_4: player[3]}) for expr
                          in utility_derivative]

    differences = written_derivative - utility_derivative
    for difference in differences:
        assert np.isclose(round(difference, 10), 0)
```

```python
opponents = [q, k]
```

```python
utility = opt_mo.tournament_utility(p, opponents)
derivative_of_utility = [sym.diff(utility, i) for i in p]
```

```python
written_derivative = get_written_derivative_of_utility(p, opponents)
```

```python
for i in tqdm.tqdm(range(4)):
    assert (written_derivative[i] - derivative_of_utility[i]).simplify() == 0
```

      0%|          | 0/4 [00:00<?, ?it/s]
     25%|██▌       | 1/4 [00:06<00:18,  6.28s/it]
     50%|█████     | 2/4 [02:27<01:33, 46.69s/it]
     75%|███████▌  | 3/4 [02:33<00:34, 34.56s/it]
    100%|██████████| 4/4 [02:34<00:00, 24.63s/it]

**Stability of defection**

**Check condition for defection stability**

```python
opponents = [q, k]
```

```python
utility = opt_mo.tournament_utility(p, opponents)
```

```python
diff_utility = [sym.diff(utility, i) for i in p]
```

```python
diff_utility_at_zero = [expr.subs({p_1: 0, p_2: 0, p_3: 0, p_4: 0}).expand() for expr in diff_utility]
```

```python
def get_derivate_for_p_zeros(opponents):
    sums = 0
    for opponent in opponents:
        lhs = opt_mo.utility.linear_term_numerator(opponent) * opt_mo.utility.constant_term_denominator(opponent)
        rhs = opt_mo.utility.linear_term_denominator(opponent) * opt_mo.utility.constant_term_numerator(opponent)

        denominator = opt_mo.utility.constant_term_denominator(opponent) ** 2

        sums +=  (lhs - rhs) / denominator

    return (sums) * (1 / len(opponents))
```

```python
expression = get_derivate_for_p_zeros(opponents)
```

```python
for i in tqdm.tqdm(range(4)):
    assert (diff_utility_at_zero[i] - expression[i]).simplify() == 0
```

    100%|██████████| 4/4 [00:03<00:00,  1.16it/s]

```python
def stackplot(plot, eco, logscale=True):

    populations = eco.population_sizes
    _, ax = plt.subplots()
    figure = ax.get_figure()
    turns = range(len(populations))
    pops = [
        [populations[iturn][ir] for iturn in turns]
        for ir in plot.result_set.ranking
    ]
    ax.stackplot(turns, *pops)

    ax.yaxis.tick_left()
    ax.yaxis.set_label_position("right")
    ax.yaxis.labelpad = 25.0

    ax.set_ylim([0.0, 1.0])
    #ax.set_xlim([0.0, 10 ** 3])
    ax.set_ylabel("Relative population size", fontweight='bold')
    ax.set_xlabel("Turn", fontweight='bold')

    trans = transforms.blended_transform_factory(ax.transAxes, ax.transData)
    ticks = []
    for i, n in enumerate(plot.result_set.ranked_names):
        x = -0.01
        y = (i + 0.5) * 1 / plot.result_set.num_players
        if n != 'Defector':
            opponent_strings = (n.split(':')[1].replace('[', '').replace(']', '')).split(",")
            opponent = [Fraction(float(op)).limit_denominator() for op in opponent_strings]
            label = '$q_{%s}$'% i
#             for p in range(3):
#                 label += r'\frac{' + str(opponent[p].numerator) + '}{' + str(opponent[p].denominator) + '},'
#             label += r'\frac{' + str(opponent[3].numerator) + '}{' + str(opponent[3].denominator) + '})$'
#             print(label)
            n = label
        ax.annotate(
            n,
            xy=(x, y),
            xycoords=trans,
            clip_on=False,
            va="center",
            ha="right",
            fontsize=10,
        )
        ticks.append(y)
    ax.set_yticks(ticks)
    ax.tick_params(direction="out")
    ax.set_yticklabels([])
    if logscale:
        ax.set_xscale("log")
    for tick in ax.yaxis.get_majorticklabels():  # example for xaxis
        tick.set_fontsize(20)

    plt.tight_layout()
    plt.xlim(0, 10 **2)
    return figure
```

```python
for seed in range(2, 10):
    np.random.seed(seed)
    opponents = [[np.round(np.random.random(), 5) for _ in range(4)] for _ in range(3)]

    derivative = get_derivate_for_p_zeros(opponents)

    if all([el < 0 for el in derivative]):
        print('Found at: %s' % seed)
        break
```

    Found at: 5

```python
defection_stable_opponents_set = [axl.MemoryOnePlayer(ps, initial=D) for ps in opponents] + [axl.Defector()]
```

```python
tournament = axl.Tournament(defection_stable_opponents_set)
results = tournament.play(progress_bar=False)
eco = axl.Ecosystem(results)
eco.reproduce(500)
```

```python
plot = axl.Plot(results)
p = stackplot(plot, eco,)
```

```python
p.savefig('../img/population_defection_takes_over.pdf', bbox_inches='tight')
```

```python
for seed in range(5000):
    np.random.seed(4)
    opponents = [[round(np.random.random(), 5) for _ in range(4)] for _ in range(3)]

    np.random.seed(seed)
    other_opponent =[[np.random.random() for _ in range(4)]]

    derivative = get_derivate_for_p_zeros(opponents + other_opponent)
    if all([el < 0 for el in derivative]):
        print('Found at: %s' % seed)
        break
```

```python
defection_stable_opponents_set = [axl.MemoryOnePlayer(ps, initial=C) for ps in opponents]
defection_stable_opponents_set += [axl.Defector()]
```

```python
tournament = axl.Tournament(defection_stable_opponents_set)
results = tournament.play(progress_bar=False)
eco = axl.Ecosystem(results)
eco.reproduce(50000)
```

```python
plot = axl.Plot(results)
p = stackplot(plot, eco)

p.savefig('../img/population_defection_fails.pdf', bbox_inches='tight')
```

```python

```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, G.F. Forsyth, C.D. Cooper. Based on [CFD Python](https://github.com/barbagroup/CFDPython), (c)2013 L.A. Barba, also under CC-BY.

# Space & Time

## Introduction to numerical solution of PDEs

Welcome to *Space and Time: Introduction to finite-difference solutions of PDEs*, the second module of ["Practical Numerical Methods with Python"](http://openedx.seas.gwu.edu/courses/GW/MAE6286/2014_fall/about).

In the first module, we looked into numerical integration methods for the solution of ordinary differential equations (ODEs), using the phugoid model of glider flight as a motivation. In this module, we will study the numerical solution of *partial differential equations (PDEs)*, where the unknown is a multi-variate function. The problem could depend on time, $t$, and one spatial dimension $x$ (or more), which means we need to build a discretization grid with each independent variable.

We will start our discussion of numerical PDEs with 1-D linear and non-linear convection equations, the 1-D diffusion equation, and 1-D Burgers' equation. We hope you will enjoy them!

## 1D linear convection

The *one-dimensional linear convection equation* is the simplest, most basic model that can be used to learn something about numerical solution of PDEs. It's surprising that this little equation can teach us so much! Here it is:

\begin{equation}\frac{\partial u}{\partial t} + c \frac{\partial u}{\partial x} = 0\end{equation}

The equation represents a *wave* propagating with speed $c$ in the $x$ direction, without change of shape. For that reason, it's sometimes called the *one-way wave equation* (sometimes also the *advection equation*).

With an initial condition $u(x,0)=u_0(x)$, the equation has an exact solution given by:

\begin{equation}u(x,t)=u_0(x-ct).
\end{equation}

Go on: check it. Take the time and space derivative and stick them into the equation to see that it holds!

Look at the exact solution for a moment ... we know two things about it:

1. its shape does not change, being always the same as the initial wave, $u_0$, only shifted in the $x$-direction; and
2. it's constant along so-called **characteristic curves**, $x-ct=$constant. This means that for any point in space and time, you can move back along the characteristic curve to $t=0$ to know the value of the solution.

#### Characteristic curves for positive wave speed.

Why do we call the equations *linear*? PDEs can be either linear or non-linear. In a linear equation, the unknown function $u$ and its derivatives appear only in linear terms, in other words, there are no products, powers, or transcendental functions applied on them.

What is the most important feature of linear equations? Do you remember? In case you forgot: solutions can be superposed to generate new solutions that still satisfy the original equation. This is super useful!

## Finite-differences

In the previous lessons, we discretized time derivatives; now we have derivatives in both space *and* time, so we need to discretize with respect to *both* these variables.

Imagine a *space-time* plot, where the coordinates in the vertical direction represent advancing in time—for example, from $t^n$ to $t^{n+1}$—and the coordinates in the horizontal direction move in space: consecutive points are $x_{i-1}$, $x_i$, and $x_{i+1}$.  This creates a grid where a point has both a temporal and spatial index. Here is a graphical representation of the space-time grid:

\begin{matrix}
t^{n+1} & \rightarrow & \bullet  && \bullet  && \bullet  \\
t^n & \rightarrow & \bullet  && \bullet  && \bullet  \\
& &  x_{i-1} && x_i && x_{i+1}
\end{matrix}

For the numerical solution of $u(x,t)$, we'll use subscripts to denote the spatial position, like $u_i$, and superscripts to denote the temporal instant, like $u^n$.  We would then label the solution at the top-middle point in the grid above as follows:
$u^{n+1}_{i}$.

Each grid point below has an index $i$, corresponding to the spatial position and increasing to the right, and an index $n$, corresponding to the time instant and increasing upwards.  A small grid segment would have the following values of the numerical solution at each point:

\begin{matrix}
& &\bullet & & \bullet & &  \bullet \\
& &u^{n+1}_{i-1} & & u^{n+1}_i & & u^{n+1}_{i+1} \\
& &\bullet & & \bullet & &  \bullet \\
& &u^n_{i-1} & & u^n_i & & u^n_{i+1} \\
& &\bullet & & \bullet & &  \bullet \\
& &u^{n-1}_{i-1} & & u^{n-1}_i & & u^{n-1}_{i+1} \\
\end{matrix}

Another way to explain our discretization grid is to say that it is built with constant steps in time and space, $\Delta t$ and $\Delta x$, as follows:

\begin{eqnarray}
x_i &=& i\, \Delta x \quad \text{and} \quad t^n= n\, \Delta t \nonumber \\
u_i^n &=& u(i\, \Delta x, n\, \Delta t)
\end{eqnarray}

### Discretizing our model equation

Let's see how to discretize the 1-D linear convection equation in both space and time.  By definition, the partial derivative with respect to time changes only with time and not with space; its discretized form changes only the $n$ indices.  Similarly, the partial derivative with respect to $x$ changes with space not time, and only the $i$ indices are affected.

We'll discretize the spatial coordinate $x$ into points indexed from $i=0$ to $N$, and then step in discrete time intervals of size $\Delta t$.

From the definition of a derivative (and simply removing the limit), we know that for $\Delta x$ sufficiently small:

\begin{equation}\frac{\partial u}{\partial x}\approx \frac{u(x+\Delta x)-u(x)}{\Delta x}\end{equation}

This formula could be applied at any point $x_i$. But note that it's not the only way that we can estimate the derivative. The geometrical interpretation of the first derivative $\partial u/ \partial x$ at any point is that it represents the slope of the tangent to the curve $u(x)$. In the sketch below, we show a slope line at $x_i$ and mark it as "exact." If the formula written above is applied at $x_i$, it approximates the derivative using the next spatial grid point: it is then called a _forward difference_ formula.

But as shown in the sketch below, we could also estimate the spatial derivative using the point behind $x_i$, in which case it is called a _backward difference_. We could even use the two points on each side of $x_i$, and obtain what's called a _central difference_ (but in that case the denominator would be $2\Delta x$).

#### Three finite-difference approximations at $x_i$.

We have three possible ways to represent a discrete form of $\partial u/ \partial x$:

* Forward difference: uses $x_i$ and $x_i + \Delta x$,
* Backward difference: uses $x_i$ and $x_i- \Delta x$,
* Central difference: uses two points on either side of $x_i$.

The sketch above also suggests that some finite-difference formulas might be better than others: it looks like the *central difference* approximation is closer to the slope of the "exact" derivative. Curious if this is just an effect of our exaggerated picture? We'll show you later how to make this observation rigorous!

The three formulas are:

\begin{eqnarray}
\frac{\partial u}{\partial x} & \approx & \frac{u(x_{i+1})-u(x_i)}{\Delta x} \quad\text{Forward}\\
\frac{\partial u}{\partial x} & \approx & \frac{u(x_i)-u(x_{i-1})}{\Delta x} \quad\text{Backward}\\
\frac{\partial u}{\partial x} & \approx & \frac{u(x_{i+1})-u(x_{i-1})}{2\Delta x} \quad\text{Central}
\end{eqnarray}

Euler's method is equivalent to using a forward-difference scheme for the time derivative. Let's stick with that, and choose the backward-difference scheme for the space derivative.  Our discrete equation is then:

\begin{equation}\frac{u_i^{n+1}-u_i^n}{\Delta t} + c \frac{u_i^n - u_{i-1}^n}{\Delta x} = 0, \end{equation}

where $n$ and $n+1$ are two consecutive steps in time, while $i-1$ and $i$ are two neighboring points of the discretized $x$ coordinate. With given initial conditions, the only unknown in this discretization is $u_i^{n+1}$.  We solve for this unknown to get an equation that lets us step in time, as follows:

\begin{equation}u_i^{n+1} = u_i^n - c \frac{\Delta t}{\Delta x}(u_i^n-u_{i-1}^n)\end{equation}

We like to make drawings of a grid segment, showing the grid points that influence our numerical solution. This is called a **stencil**. Below is the stencil for solving our model equation with the finite-difference formula we wrote above.

#### Stencil for the "forward-time/backward-space" scheme.

## And compute!

Alright. Let's get a little Python on the road. First: we need to load our array and plotting libraries, as usual. And if you noticed in the [*Bonus!* notebook for Module 1](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/01_phugoid/01_04_Second_Order_Methods.ipynb), we taught you a neat trick to set some global plotting parameters with the `rcParams` module. We like to do that.

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

As a first exercise, we'll solve the 1D linear convection equation with a *square wave* initial condition, defined as follows:

\begin{equation}
u(x,0)=\begin{cases}2 & \text{where } 0.5\leq x \leq 1,\\
1 & \text{everywhere else in } (0, 2)
\end{cases}
\end{equation}

We also need a boundary condition on $x$: let $u=1$ at $x=0$. Our spatial domain for the numerical solution will only cover the range $x\in (0, 2)$.

#### Square wave initial condition.

Now let's define a few variables; we want to make an evenly spaced grid of points within our spatial domain. In the code below, we define a variable called `nx` that will be the number of spatial grid points, and a variable `dx` that will be the distance between any pair of adjacent grid points. We also can define a step in time, `dt`, a number of steps, `nt`, and a value for the wave speed: we like to keep things simple and make $c=1$.

```python
nx = 41  # try changing this number from 41 to 81 and Run All ... what happens?
dx = 2/(nx-1)
nt = 25
dt = .02
c = 1      #assume wavespeed of c = 1
x = numpy.linspace(0,2,nx)
```

We also need to set up our initial conditions. Here, we use the NumPy function `ones()` defining an array which is `nx` elements long with every value equal to $1$. How useful! We then *change a slice* of that array to the value $u=2$, to get the square wave, and we print out the initial array just to admire it. But which values should we change?  The problem states that we need to change the indices of `u` such that the square wave begins at $x = 0.5$ and ends at $x = 1$.

We can use the `numpy.where` function to return a list of indices where the vector $x$ meets (or doesn't meet) some condition.

```python
u = numpy.ones(nx)      #numpy function ones()
lbound = numpy.where(x >= 0.5)
ubound = numpy.where(x <= 1)

print(lbound)
print(ubound)
```

    (array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
           27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]),)
    (array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
           17, 18, 19, 20]),)

That leaves us with two vectors.  `lbound`, which has the indices for $x \geq .5$ and 'ubound', which has the indices for $x \leq 1$.  To combine these two, we can use an intersection, with `numpy.intersect1d`.

```python
bounds = numpy.intersect1d(lbound, ubound)
u[bounds]=2  #setting u = 2 between 0.5 and 1 as per our I.C.s
print(u)
```

    [ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  2.  2.  2.  2.  2.  2.  2.  2.
      2.  2.  2.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.  1.
      1.  1.  1.  1.  1.]

Remember that Python can also combine commands, we could have instead written
```Python
u[numpy.intersect1d(numpy.where(x >= 0.5), numpy.where(x <= 1))] = 2
```

but that can be a little hard to read.

Now let's take a look at those initial conditions we've built with a handy plot.

```python
pyplot.plot(x, u, color='#003366', ls='--', lw=3)
pyplot.ylim(0,2.5);
```

It does look pretty close to what we expected. But it looks like the sides of the square wave are not perfectly vertical. Is that right? Think for a bit.

Now it's time to write some code for the discrete form of the convection equation using our chosen finite-difference scheme.

For every element of our array `u`, we need to perform the operation:

$$u_i^{n+1} = u_i^n - c \frac{\Delta t}{\Delta x}(u_i^n-u_{i-1}^n)$$

We'll store the result in a new (temporary) array `un`, which will be the solution $u$ for the next time-step.  We will repeat this operation for as many time-steps as we specify and then we can see how far the wave has traveled.

We first initialize the placeholder array `un` to hold the values we calculate for the $n+1$ timestep, using once again the NumPy function `ones()`.

Then, we may think we have two iterative operations: one in space and one in time (we'll learn differently later), so we may start by nesting a spatial loop inside the time loop, as shown below. You see that the code for the finite-difference scheme is a direct expression of the discrete equation:

```python
for n in range(1,nt):
    un = u.copy()
    for i in range(1,nx):

        u[i] = un[i]-c*dt/dx*(un[i]-un[i-1])
```

**Note**—We will learn later that the code as written above is quite inefficient, and there are better ways to write this, Python-style. But let's carry on.

Now let's inspect our solution array after advancing in time with a line plot.

```python
pyplot.plot(x, u, color='#003366', ls='--', lw=3)
pyplot.ylim(0,2.5);
```

That's funny. Our square wave has definitely moved to the right, but it's no longer in the shape of a top-hat.  **What's going on?**

##### Dig deeper

The solution differs from the expected square wave because the discretized equation is an approximation of the continuous differential equation that we want to solve. There are errors: we knew that. But the modified shape of the initial wave is something curious. Maybe it can be improved by making the grid spacing finer. Why don't you try it? Does it help?

## Spatial truncation error

Recall the finite-difference approximation we are using for the spatial derivative:

\begin{equation}\frac{\partial u}{\partial x}\approx \frac{u(x+\Delta x)-u(x)}{\Delta x}\end{equation}

We obtain it by using the definition of the derivative at a point, and simply removing the limit, in the assumption that $\Delta x$ is very small. But we already learned with Euler's method that this introduces an error, called the *truncation error*.

Using a Taylor series expansion for the spatial terms now, we see that the backward-difference scheme produces a first-order method, in space.

\begin{equation}
\frac{\partial u}{\partial x}(x_i) = \frac{u(x_i)-u(x_{i-1})}{\Delta x} + \frac{\Delta x}{2} \frac{\partial^2 u}{\partial x^2}(x_i) - \frac{\Delta x^2}{6} \frac{\partial^3 u}{\partial x^3}(x_i)+ \cdots
\end{equation}

The dominant term that is neglected in the finite-difference approximation is of $\mathcal{O}(\Delta x)$. We also see that the approximation *converges* to the exact derivative as $\Delta x \rightarrow 0$. That's good news!

In summary, the chosen "forward-time/backward space" difference scheme is first-order in both space and time: the truncation errors are $\mathcal{O}(\Delta t, \Delta x)$. We'll come back to this!

## Non-linear convection

Let's move on to the non-linear convection equation, using the same methods as before. The 1-D convection equation is:

\begin{equation}\frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} = 0\end{equation}

The only difference with the linear case is that we've replaced the constant wave speed $c$ by the variable speed $u$. The equation is non-linear because now we have a product of the solution and one of its derivatives: the product $u\,\partial u/\partial x$. This changes everything!

We're going to use the same discretization as for linear convection: forward difference in time and backward difference in space. Here is the discretized equation:

\begin{equation}\frac{u_i^{n+1}-u_i^n}{\Delta t} + u_i^n \frac{u_i^n-u_{i-1}^n}{\Delta x} = 0\end{equation}

Solving for the only unknown term, $u_i^{n+1}$, gives an equation that can be used to advance in time:

\begin{equation}u_i^{n+1} = u_i^n - u_i^n \frac{\Delta t}{\Delta x} (u_i^n - u_{i-1}^n)\end{equation}

There is very little that needs to change from the code written so far. In fact, we'll even use the same square-wave initial condition. But let's re-initialize the variable `u` with the initial values, and re-enter the numerical parameters here, for convenience (we no longer need $c$, though).

```python
##problem parameters
nx = 41
dx = 2/(nx-1)
nt = 10
dt = .02

##initial conditions
u = numpy.ones(nx)
u[numpy.intersect1d(lbound, ubound)]=2

```

 How does it look?

```python
pyplot.plot(x, u, color='#003366', ls='--', lw=3)
pyplot.ylim(0,2.5);
```

Changing just one line of code in the solution of linear convection, we are able to now get the non-linear solution: the line that corresponds to the discrete equation now has `un[i]` in the place where before we just had `c`. So you could write something like:
```Python
for n in range(1,nt):
  un = u.copy()
  for i in range(1,nx):
    u[i] = un[i]-un[i]*dt/dx*(un[i]-un[i-1])
```

We're going to be more clever than that and use NumPy to update all values of the spatial grid in one fell swoop. We don't really need to write a line of code that gets executed *for each* value of $u$ on the spatial grid. Python can update them all at once! Study the code below, and compare it with the one above. Here is a helpful sketch, to illustrate the array operation—also called a "vectorized" operation—for $u_i-u_{i-1}$.

<br>
#### Sketch to explain vectorized stencil operation.  Adapted from ["Indices point between elements"](https://blog.nelhage.com/2015/08/indices-point-between-elements/) by Nelson Elhage.

```python
for n in range(1, nt):
    un = u.copy()
    u[1:] = un[1:]-un[1:]*dt/dx*(un[1:]-un[0:-1])
    u[0] = 1.0
```

```python
pyplot.plot(x, u, color='#003366', ls='--', lw=3)
pyplot.ylim(0,2.5);
```

Hmm. That's quite interesting: like in the linear case, we see that we have lost the sharp sides of our initial square wave, but there's more. Now, the wave has also lost symmetry! It seems to be lagging on the rear side, while the front of the wave is steepening. Is this another form of numerical error, do you ask? No! It's physics!

##### Dig deeper

Think about the effect of having replaced the constant wave speed $c$ by the variable speed given by the solution $u$. It means that different parts of the wave move at different speeds. Make a sketch of an initial wave and think about where the speed is higher and where it is lower ...

## References

* Elhage, Nelson (2015), ["Indices point between elements"](https://blog.nelhage.com/2015/08/indices-point-between-elements/)

---

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

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, "r").read())
```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</style>


# Calculating Receiver Position from Android Raw GNSS Measurements

*This notebook was written by [Mitchell Johnson](https://www.johnsonmitchelld.com/). The source code, as well as the gnssutils Python package containing the EphemerisManager module, can be found on [Github](https://github.com/johnsonmitchelld/gnss-analysis).*

Satellite navigation is absolutely fascinating. The GPS receiver in your smartphone uses concepts as diverse as orbital mechanics, digital signal processing, convex optimization, and the theory of relativity to determine your location anywhere on Earth within seconds. From the development of GPS in the 80's until the early 2010s, satellite navigation was a specialized tool, used primarily by the military, airlines, surveyors, and private citizens fortunate enough to own an expensive personal receiver. Widespread GNSS receiver adoption in smartphones over the past decade, combined with the development of new GNSS constellations from China and the EU, have commoditized satellite navigation like never before.

Despite these changes, GNSS receivers were mostly black boxes until recently. Most standalone receivers output only a final position, velocity, and time solution, with dilution of precision and satellite signal-to-noise ratios included if you're lucky.

That changed in 2016 with Google's introduction of the [raw GNSS measurement API](https://developer.android.com/guide/topics/sensors/gnss) in Android 7. Several applications have since been developed to make use of this data, including Google's own [GnssLogger](https://play.google.com/store/apps/details?id=com.google.android.apps.location.gps.gnsslogger&hl=en_US&gl=US), which provides an interface for logging raw GNSS measurements to a text file. It is now possible for any of the billions of Android device owners worldwide to experiment with raw GNSS measurement data for themselves.

What's still missing is an approachable, easy-to-use platform for processing this data. Google's recent paper, Android Raw GNSS Measurement Datasets for Precise Positioning, contains the following table listing publicly available software packages for GNSS positioning:

Notice that all of the open source options are either written in C/C++ or MATLAB. C and C++ are highly performant languages, but neither is particularly approachable without a software engineering background. MATLAB is much more familiar to engineers and scientists, but I believe its widespread use is harmful for reasons I outline [here](https://www.johnsonmitchelld.com/opinion/2020/12/20/stop-teaching-matlab.html).

To address this void in approachable GNSS software packages built on open source platforms, I decided to develop my own. This Jupyter notebook, along with the accompanying EphemerisManager class, provide everything a user needs to calculate the position of their Android phone using recorded GNSS measurements. All that's necessary (in addition to the phone, of course) is an environment capable of running Jupyter notebooks.

I would like to thank the developers of the tools listed above. I used Google's [GPS Measurement Tools](https://github.com/google/gps-measurement-tools) and the European Space Agency's [GNSS resources](https://gage.upc.edu/tutorials/) as references while developing this project, in addition to many other sources mentioned below.

## Data Aquisition

The experiment below was conducted with a sample dataset I collected with Google's GnssLogger app on my Samsung Galaxy S9 Plus while driving through downtown Seattle. If you'd like to collect your own data and process it with this notebook, you can find the code and documentation [here](https://github.com/johnsonmitchelld/gnss-analysis).

The import process and data directory determination below assume that the Python interpreter's working directory is the same as this notebook, and that the gnssutils package and data repository are located in the parent directory. If that is not the case, you'll have to modify accordingly.

```python
import sys, os, csv
parent_directory = os.path.split(os.getcwd())[0]
ephemeris_data_directory = os.path.join(parent_directory, 'data')
sys.path.insert(0, parent_directory)
from datetime import datetime, timezone, timedelta
import pandas as pd
import numpy as np
import navpy
from gnssutils import EphemerisManager
```

Let's read in a GNSS log file, format the satellite IDs to match up with the RINEX 3.0 standard, and convert the columns we need to calculate receiver position to numeric values. We'll also filter the data so that we're only working with GPS measurements to simplify the rest of the analysis.

```python
# Get path to sample file in data directory, which is located in the parent directory of this notebook
input_filepath = os.path.join(parent_directory, 'data', 'sample', 'gnss_log_2020_12_02_17_19_39.txt')

with open(input_filepath) as csvfile:
    reader = csv.reader(csvfile)
    for row in reader:
        if row[0][0] == '#':
            if 'Fix' in row[0]:
                android_fixes = [row[1:]]
            elif 'Raw' in row[0]:
                measurements = [row[1:]]
        else:
            if row[0] == 'Fix':
                android_fixes.append(row[1:])
            elif row[0] == 'Raw':
                measurements.append(row[1:])

android_fixes = pd.DataFrame(android_fixes[1:], columns = android_fixes[0])
measurements = pd.DataFrame(measurements[1:], columns = measurements[0])

# Format satellite IDs
measurements.loc[measurements['Svid'].str.len() == 1, 'Svid'] = '0' + measurements['Svid']
measurements.loc[measurements['ConstellationType'] == '1', 'Constellation'] = 'G'
measurements.loc[measurements['ConstellationType'] == '3', 'Constellation'] = 'R'
measurements['SvName'] = measurements['Constellation'] + measurements['Svid']

# Remove all non-GPS measurements
measurements = measurements.loc[measurements['Constellation'] == 'G']

# Convert columns to numeric representation
measurements['Cn0DbHz'] = pd.to_numeric(measurements['Cn0DbHz'])
measurements['TimeNanos'] = pd.to_numeric(measurements['TimeNanos'])
measurements['FullBiasNanos'] = pd.to_numeric(measurements['FullBiasNanos'])
measurements['ReceivedSvTimeNanos']  = pd.to_numeric(measurements['ReceivedSvTimeNanos'])
measurements['PseudorangeRateMetersPerSecond'] = pd.to_numeric(measurements['PseudorangeRateMetersPerSecond'])
measurements['ReceivedSvTimeUncertaintyNanos'] = pd.to_numeric(measurements['ReceivedSvTimeUncertaintyNanos'])

# A few measurement values are not provided by all phones
# We'll check for them and initialize them with zeros if missing
if 'BiasNanos' in measurements.columns:
    measurements['BiasNanos'] = pd.to_numeric(measurements['BiasNanos'])
else:
    measurements['BiasNanos'] = 0
if 'TimeOffsetNanos' in measurements.columns:
    measurements['TimeOffsetNanos'] = pd.to_numeric(measurements['TimeOffsetNanos'])
else:
    measurements['TimeOffsetNanos'] = 0

print(measurements.columns)
```

    Index(['ElapsedRealtimeMillis', 'TimeNanos', 'LeapSecond',
           'TimeUncertaintyNanos', 'FullBiasNanos', 'BiasNanos',
           'BiasUncertaintyNanos', 'DriftNanosPerSecond',
           'DriftUncertaintyNanosPerSecond', 'HardwareClockDiscontinuityCount',
           'Svid', 'TimeOffsetNanos', 'State', 'ReceivedSvTimeNanos',
           'ReceivedSvTimeUncertaintyNanos', 'Cn0DbHz',
           'PseudorangeRateMetersPerSecond',
           'PseudorangeRateUncertaintyMetersPerSecond',
           'AccumulatedDeltaRangeState', 'AccumulatedDeltaRangeMeters',
           'AccumulatedDeltaRangeUncertaintyMeters', 'CarrierFrequencyHz',
           'CarrierCycles', 'CarrierPhase', 'CarrierPhaseUncertainty',
           'MultipathIndicator', 'SnrInDb', 'ConstellationType', 'AgcDb',
           'CarrierFrequencyHz', 'Constellation', 'SvName'],
          dtype='object')

## Pre-processing

Those with experience in GNSS data processing will probably notice that we're missing a few important fields for calculating receiver position. For each satellite the process requires:

* **Time of signal transmission** - in order to calculate the satellite's location when the signal was transmitted
* **Measured pseudorange** - a rough estimate of the distance between each satellite and the receiver before correcting for clock biases, ionospheric delay, and other phenomena which we'll explain later
* **Ephemeris parameters** - a set of parameters required to calculate the satellite's position in space

Of these, the only one provided directly by Android's raw GNSS measurement API is the time of signal transmission, labeled `ReceivedSvTimeNanos`. Fortunately, the European Global Navigation Satellite Systems Agency (GSA) published a very helpful [white paper](https://www.gsa.europa.eu/system/files/reports/gnss_raw_measurement_web_0.pdf) explaining the parameters reported by Android and how they can be used to generate the values we need.

Ephemeris parameters are an exception. The Android API technically has commands for obtaining these, but they don't seem to work on my Galaxy S9 - at least not in the GnssLogger app. Instead we'll get them from the [International GNSS Service](https://igs.org/mgex/data-products/#data) (IGS), which maintains an array of GNSS data products available to the public through NASA's Crustal Dynamics Data Information System, the German Bundesamt für Kartographie und Geodäsie (BKG), and the French Institut Géographique National (IGN). More on this later.

### Timestamp Generation

First things first: what time were these measurements taken?

It turns out this question isn't as simple as it might sound. The GPS system has its own time scale, which differs from UTC by some number of leap seconds. Leap seconds are used to adjust UTC for changes in the rotation of the Earth, and are decided by the International Earth Rotation and Reference Systems Service. If you're interested in reading more about leap seconds and international time standards, you can check out this [FAQ](https://www.nist.gov/pml/time-and-frequency-division/nist-time-frequently-asked-questions-faq) from the US National Institute of Standards and Technology.

We're going to need a Python `datetime` timestamp for each measurement in order to determine which ephemeris files to retrieve from IGS. To get one, let's calculate the GPS time in nanoseconds using the equations from Section 2.4 of the GSA's white paper. Then we'll convert it to a normal Unix timestamp using the `Pandas.to_datetime()` function with the GPS reference epoch.

Note that the UTC flag is set to True in the `Pandas.to_datetime()` method. The timestamp column needs to be in UTC because the ephemeris files provided by IGS are labeled by UTC date and time. Since Python's datetime library defaults to local time on some systems, it was necessary to keep everything timezone-aware to avoid mix-ups.

This nomenclature is a bit confusing since we're using the `to_datetime()` method to create a column called `UnixTime`, not `Utc`. Unix time and UTC are technically in the same time zone, but UTC includes leap seconds, while Unix time does not. Since we're creating a timestamp from the number of seconds elapsed since the GPS epoch, the resulting timestamp could differ from UTC by some number of leap seconds. That's close enough to ensure we get the correct ephemeris file, and it won't affect the actual position calculations because we don't use the `UnixTime` column for those.

Finally, let's split the data into measurement epochs. We do this by creating a new column and setting it to 1 whenever the difference between a timestamp and the previous timestamp is greater than 200 milliseconds using the `DataFrame.shift()` command. Then we use the `Series.cumsum()` method to generate unique numbers for the individual epochs.

```python

measurements['GpsTimeNanos'] = measurements['TimeNanos'] - (measurements['FullBiasNanos'] - measurements['BiasNanos'])
gpsepoch = datetime(1980, 1, 6, 0, 0, 0)
measurements['UnixTime'] = pd.to_datetime(measurements['GpsTimeNanos'], utc = True, origin=gpsepoch)
measurements['UnixTime'] = measurements['UnixTime']

# Split data into measurement epochs
measurements['Epoch'] = 0
measurements.loc[measurements['UnixTime'] - measurements['UnixTime'].shift() > timedelta(milliseconds=200), 'Epoch'] = 1
measurements['Epoch'] = measurements['Epoch'].cumsum()
```

Next let's calculate the estimated signal transmission and reception times, once again using the equations found in Section 2.4 of the GSA white paper mentioned above. Once we've determined these, we can calculate a measured pseudorange value for each satellite by taking the difference between the two and multiplying by the speed of light:

$$
\rho_{measured} = \frac{(t_{Rx} - t_{Tx})}{1E9} \cdot c
$$

```python
WEEKSEC = 604800
LIGHTSPEED = 2.99792458e8

# This should account for rollovers since it uses a week number specific to each measurement

measurements['tRxGnssNanos'] = measurements['TimeNanos'] + measurements['TimeOffsetNanos'] - (measurements['FullBiasNanos'].iloc[0] + measurements['BiasNanos'].iloc[0])
measurements['GpsWeekNumber'] = np.floor(1e-9 * measurements['tRxGnssNanos'] / WEEKSEC)
measurements['tRxSeconds'] = 1e-9*measurements['tRxGnssNanos'] - WEEKSEC * measurements['GpsWeekNumber']
measurements['tTxSeconds'] = 1e-9*(measurements['ReceivedSvTimeNanos'] + measurements['TimeOffsetNanos'])
# Calculate pseudorange in seconds
measurements['prSeconds'] = measurements['tRxSeconds'] - measurements['tTxSeconds']

# Conver to meters
measurements['PrM'] = LIGHTSPEED * measurements['prSeconds']
measurements['PrSigmaM'] = LIGHTSPEED * 1e-9 * measurements['ReceivedSvTimeUncertaintyNanos']
```

Now that we have pseudorange values, we can begin the standard process for calculating receiver position. First we need to retrieve the ephemeris data for each satellite from one of the International GNSS Service (IGS) analysis centers. These include NASA's [CDDIS](https://cddis.nasa.gov/Data_and_Derived_Products/GNSS/orbit_products.html) and the German [BKG](https://igs.bkg.bund.de/dataandproducts/index).

This process is, frankly, a pain, so I wrote a Python module to handle the details. You can view the code and documentation [here](https://github.com/johnsonmitchelld/gnss-analysis/tree/main/gnssutils), but all you have to do is initialize an instance of the `EphemerisManager` class with a path to the directory where you want it to cache downloaded files. Then, provide the `get_ephemeris()` method with a Python datetime object and and list of satellites, and it will return a Pandas dataframe of valid ephemeris parameters for the requested satellites.

Broadcast ephemerides are good for four hours from the time they are issued, but the GPS control segment generally uploads new values every two hours. The `EphemerisManager` class downloads a whole day's worth of data at a time and then returns the most recent parameters at the requested time for each satellite. Because of this we're going to run the `get_ephemeris()` method for every epoch in case new parameters were issued since the last measurement was taken.

For the sake of demonstration, let's start by walking through the position solution for just one measurement epoch. We'll use the first epoch in the dataset with measurements from at least five satellites.

```python
manager = EphemerisManager(ephemeris_data_directory)

epoch = 0
num_sats = 0
while num_sats < 5 :
    one_epoch = measurements.loc[(measurements['Epoch'] == epoch) & (measurements['prSeconds'] < 0.1)].drop_duplicates(subset='SvName')
    timestamp = one_epoch.iloc[0]['UnixTime'].to_pydatetime(warn=False)
    one_epoch.set_index('SvName', inplace=True)
    num_sats = len(one_epoch.index)
    epoch += 1

sats = one_epoch.index.unique().tolist()
ephemeris = manager.get_ephemeris(timestamp, sats)
print(timestamp)
print(one_epoch[['UnixTime', 'tTxSeconds', 'GpsWeekNumber']])
```

    2020-12-03 01:19:57.432062+00:00
                                      UnixTime     tTxSeconds  GpsWeekNumber
    SvName
    G05    2020-12-03 01:19:57.432062976+00:00  350397.354393         2134.0
    G07    2020-12-03 01:19:57.432062976+00:00  350397.362980         2134.0
    G08    2020-12-03 01:19:57.432062976+00:00  350397.359065         2134.0
    G09    2020-12-03 01:19:57.432062976+00:00  350397.356830         2134.0
    G13    2020-12-03 01:19:57.432062976+00:00  350397.353015         2134.0
    G14    2020-12-03 01:19:57.432062976+00:00  350397.359692         2134.0
    G27    2020-12-03 01:19:57.432062976+00:00  350397.354134         2134.0
    G28    2020-12-03 01:19:57.432062976+00:00  350397.358284         2134.0
    G30    2020-12-03 01:19:57.432062976+00:00  350397.363215         2134.0

Okay, we've got valid ephemeris parameters and signal transmission time in seconds of the GPS week for one measurement epoch's worth of data. Time to figure out where these satellites are (or where they were at approximately 01:19:57.432 UTC on December 3rd, 2020).

## Coordinate Systems Primer

A brief geometry lesson is in order before I continue. If you ask someone on the street what their location is, they will most likely provide you with some sort of named landmark for reference, e.g. a street name, intersection, or address. Not every location on Earth has a named landmark nearby for reference, so most children learn about the concept of latitude and longitude in school.

### LLA

Latitude, longitude, and altitude (LLA from here on) together make up a [spherical coordinate system](https://en.wikipedia.org/wiki/Spherical_coordinate_system), which was developed over millennia by geographers for use in celestial navigation. Latitude, longitude and altitude are handy because they quickly give you an idea of where you are generally on Earth - positive latitudes are in the northern hemisphere, positive longitudes west of the Prime Meridian, etc. However, spherical coordinates tend to complicate the math involved with satellite navigation because they are not linear. One degree of latitude equals roughly 69 miles (111 km) at the equator but only 49 miles (79 km) at 45 degrees north or south.

### ECEF

The Earth-centered, Earth-fixed ([ECEF](https://en.wikipedia.org/wiki/ECEF)) coordinate system solves this issue. It is a Cartesian coordinate system with coordinates on X, Y, and Z axes which rotate with the Earth. Satellite navigation systems perform most of their calculations in ECEF coordinates before converting to LLA for user output; hence our function for calculating satellite position will output ECEF coordinates.

### ENU/NED

Another challenge arises when calculating the vector **between** two points on Earth. A linear coordinate system is also desired in this case. However, ECEF doesn't quite work for relative positioning on Earth's surface, because, for example, the X and Y axes can point east, west, up, down, or somewhere in between depending on your location. Hence a third system called [local tangent plane coordinates](https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates) is used to define vectors between coordinates. The most common of these are east, north, up (ENU) and north, east, down (NED), which we will use later.

Calculating satellite position from ephemeris data is a bit involved, but the details are conveniently laid out in Tables 20-III and 20-IV of the GPS [Interface Specification Document](https://www.gps.gov/technical/icwg/IS-GPS-200L.pdf). The function below performs this calculation, taking a Pandas dataframe with the ephemeris parameters for every satellite and a dataseries containing the transmit time for all received signals in seconds since the start of the current GPS week. It also calculates the satellite clock offsets for the given transmission time.

```python
def calculate_satellite_position(ephemeris, transmit_time):
    mu = 3.986005e14
    OmegaDot_e = 7.2921151467e-5
    F = -4.442807633e-10
    sv_position = pd.DataFrame()
    sv_position['sv']= ephemeris.index
    sv_position.set_index('sv', inplace=True)
    sv_position['t_k'] = transmit_time - ephemeris['t_oe']
    A = ephemeris['sqrtA'].pow(2)
    n_0 = np.sqrt(mu / A.pow(3))
    n = n_0 + ephemeris['deltaN']
    M_k = ephemeris['M_0'] + n * sv_position['t_k']
    E_k = M_k
    err = pd.Series(data=[1]*len(sv_position.index))
    i = 0
    while err.abs().min() > 1e-8 and i < 10:
        new_vals = M_k + ephemeris['e']*np.sin(E_k)
        err = new_vals - E_k
        E_k = new_vals
        i += 1

    sinE_k = np.sin(E_k)
    cosE_k = np.cos(E_k)
    delT_r = F * ephemeris['e'].pow(ephemeris['sqrtA']) * sinE_k
    delT_oc = transmit_time - ephemeris['t_oc']
    sv_position['delT_sv'] = ephemeris['SVclockBias'] + ephemeris['SVclockDrift'] * delT_oc + ephemeris['SVclockDriftRate'] * delT_oc.pow(2)

    v_k = np.arctan2(np.sqrt(1-ephemeris['e'].pow(2))*sinE_k,(cosE_k - ephemeris['e']))

    Phi_k = v_k + ephemeris['omega']

    sin2Phi_k = np.sin(2*Phi_k)
    cos2Phi_k = np.cos(2*Phi_k)

    du_k = ephemeris['C_us']*sin2Phi_k + ephemeris['C_uc']*cos2Phi_k
    dr_k = ephemeris['C_rs']*sin2Phi_k + ephemeris['C_rc']*cos2Phi_k
    di_k = ephemeris['C_is']*sin2Phi_k + ephemeris['C_ic']*cos2Phi_k

    u_k = Phi_k + du_k

    r_k = A*(1 - ephemeris['e']*np.cos(E_k)) + dr_k

    i_k = ephemeris['i_0'] + di_k + ephemeris['IDOT']*sv_position['t_k']

    x_k_prime = r_k*np.cos(u_k)
    y_k_prime = r_k*np.sin(u_k)

    Omega_k = ephemeris['Omega_0'] + (ephemeris['OmegaDot'] - OmegaDot_e)*sv_position['t_k'] - OmegaDot_e*ephemeris['t_oe']

    sv_position['x_k'] = x_k_prime*np.cos(Omega_k) - y_k_prime*np.cos(i_k)*np.sin(Omega_k)
    sv_position['y_k'] = x_k_prime*np.sin(Omega_k) + y_k_prime*np.cos(i_k)*np.cos(Omega_k)
    sv_position['z_k'] = y_k_prime*np.sin(i_k)
    return sv_position

# Run the function and check out the results:
sv_position = calculate_satellite_position(ephemeris, one_epoch['tTxSeconds'])
print(sv_position)
```

                 t_k   delT_sv           x_k           y_k           z_k
    sv
    G05  4797.354393 -0.000027 -2.169285e+07  4.568031e+06  1.461087e+07
    G07  4797.362980 -0.000426 -3.714100e+06 -1.642940e+07  2.075276e+07
    G08  4797.359065 -0.000002  7.360204e+06 -1.916176e+07  1.671180e+07
    G09  4797.356830 -0.000297 -7.513898e+06 -2.535861e+07  2.249150e+06
    G13  4797.353015  0.000071 -1.324738e+07  1.036799e+07  2.038926e+07
    G14  4797.359692  0.000052 -2.110816e+07 -1.395532e+07  8.071176e+06
    G27  4797.354134 -0.000027  1.340518e+07 -7.095042e+06  2.161816e+07
    G28  4797.358284  0.000648 -2.237537e+07 -1.338044e+07  5.115684e+06
    G30  4797.363215 -0.000346 -1.305779e+07 -9.338017e+06  2.122874e+07

We now have raw pseudorange values, satellite positions, and satellite clock offsets.

These pseudoranges can be repsresented by the equation ([Misra and Enge, 2006](https://www.gpstextbook.com/)):

$$
\begin{equation}
\rho^{(k)}_{measured} = \sqrt{(x^{(k)} - x_r)^2 + (y^{(k)} - y_r)^2 + (z^{(k)} - z_r)^2} + c \cdot \delta t_r  - c \cdot \delta t^{(k)} + I^{(k)}(t) + T^{(k)}(t) + \epsilon_\rho^{(k)}
\end{equation}
$$

where $x_r$, $y_r$, $z_r$, and $ \delta t_r $  are the receiver position (in ECEF coordinates) and clock bias (in seconds), $x^{(k)}$, $y^{(k)}$, $z^{(k)}$, and $ \delta t^{(k)} $ are the position and clock bias of the $k$th satellite, $c$ is the speed of light, and $I^{(k)}(t)$ and $T^{(k)}(t)$ are the ionospheric and trophospheric delay terms for the $k$th satellite. $\epsilon_\rho^{(k)}$ accounts for all remaining measurement and modeling errors. Following convention in the GNSS community, a superscript is used to identify measurements from a specific satellite, with parenthesis to distinguish between superscripts and exponents.

Since we've calculated the satellite clock offsets, let's go ahead and apply them to generate a corrected pseudorange. We're also going to ignore ionospheric and trophospheric delays, thus lumping them into the error term $\epsilon_\rho^{(k)}$. The expression for the corrected pseudorange then becomes:

$$
\rho^{(k)}_{corrected} = \rho^{(k)} + c \cdot \delta t^{(k)} = \sqrt{(x^{(k)} - x_r)^2 + (y^{(k)} - y_r)^2 + (z^{(k)} - z_r)^2} + c \cdot \delta t_r + \epsilon_\rho^{(k)}
$$

```python
#initial guesses of receiver clock bias and position
b0 = 0
x0 = np.array([0, 0, 0])
xs = sv_position[['x_k', 'y_k', 'z_k']].to_numpy()

# Apply satellite clock bias to correct the measured pseudorange values
pr = one_epoch['PrM'] + LIGHTSPEED * sv_position['delT_sv']
pr = pr.to_numpy()
```

The following is adapted from ([Blewitt, 1997](http://www.nbmg.unr.edu/staff/pdfs/blewitt%20basics%20of%20gps.pdf)).

Let's drop the subscript for the corrected pseudorange measurements and refer to them simply as $p^{(k)}$. With four or more linearly independent pseudorange measurements, we end up with the system of equations:

$$
\begin{gather*}
\rho^{(1)} = \sqrt{(x^{(1)} - x_r)^2 + (y^{(1)} - y_r)^2 + (z^{(1)} - z_r)^2} + c \cdot \delta t_r + \epsilon_\rho^{(1)} \\
\rho^{(2)} = \sqrt{(x^{(2)} - x_r)^2 + (y^{(2)} - y_r)^2 + (z^{(2)} - z_r)^2} + c \cdot \delta t_r + \epsilon_\rho^{(2)} \\
\rho^{(3)} = \sqrt{(x^{(3)} - x_r)^2 + (y^{(3)} - y_r)^2 + (z^{(3)} - z_r)^2} + c \cdot \delta t_r + \epsilon_\rho^{(3)} \\
\cdots \\
\rho^{(k)} = \sqrt{(x^{(k)} - x_r)^2 + (y^{(k)} - y_r)^2 + (z^{(k)} - z_r)^2} + c \cdot \delta t_r + \epsilon_\rho^{(k)}
\end{gather*}
$$

where $x_r, y_r, z_r$, and $\delta t_r$ are the unkowns. We will substitute the receiver clock bias term with an equivalent value in meters:

$$
b = c \cdot \delta t_r
$$

Since the system is nonlinear, we must approximate the solution using the [Gauss-Newton algorithm](https://en.wikipedia.org/wiki/Gauss%E2%80%93Newton_algorithm) for solving [non-linear least squares](https://en.wikipedia.org/wiki/Non-linear_least_squares) problems. Let us define a function, $P$, for calculating an estimated or modeled pseudorange $\hat{\rho}^{(k)}$ based on estimates $\hat{x}, \hat{y}, \hat{z}, \hat{b}$ of the real position and clock offset values $x, y, z, b$:

$$
\hat{\rho}^{(k)} = P(\hat{x}, \hat{y}, \hat{z}, \hat{b}, k) = \sqrt{(x^{(k)}\ - \hat{x})^2 + (y^{(k)} - \hat{y})^2 + (z^{(k)} - \hat{z})} + \hat{b}
$$

We perform a first-order Taylor series expansion about our estimated values:

$$
P(x, y, z, b, k) = P(\hat{x}, \hat{y}, \hat{z}, \hat{b}, k) + P'(\hat{x}, \hat{y}, \hat{z}, \hat{b}, k)(x - \hat{x}, y - \hat{y}, z - \hat{z}, b - \hat{b})
$$

where $P(x, y, z, b, k)$ is equal to our measured or observed psedurange $\rho^{(k)}$. The difference between the observed and computed pseudorange is therefore:

$$
\Delta \rho = P(x, y, z, b, k) - P(\hat{x}, \hat{y}, \hat{z}, \hat{b}, k) = P'(\hat{x}, \hat{y}, \hat{z}, \hat{b}, k)(x - \hat{x}, y - \hat{y}, z - \hat{z}, b - \hat{b})
$$

Converting to matrix form and adding the error term $\epsilon$, we have:

$$
\Delta \rho= \begin{pmatrix} \frac{\delta \rho}{\delta x} & \frac{\delta \rho}{\delta y} &
\frac{\delta \rho}{\delta z} & \frac{\delta \rho}{\delta b} \end{pmatrix}
\begin{pmatrix} \Delta x \\ \Delta y \\ \Delta z \\ \Delta b \end{pmatrix} + \epsilon
$$

Thus for $k$ satellites, we now have the system of $k$ linear equations:

$$
\begin{pmatrix} \Delta \rho^{(1)} \\
\Delta \rho^{(2)} \\
\Delta \rho^{(3)} \\
\vdots \\
\Delta \rho^{(k)}
\end{pmatrix}
=
\begin{pmatrix} \frac{\delta P^{(1)}}{\delta x} & \frac{\delta P^{(1)}}{\delta y} &
\frac{\delta \rho^{(1)}}{\delta z} & \frac{\delta \rho^{(1)}}{\delta b} \\
\frac{\delta \rho^{(2)}}{\delta x} & \frac{\delta \rho^{(2)}}{\delta y} &
\frac{\delta \rho^{(2)}}{\delta z} & \frac{\delta \rho^{(2)}}{\delta b} \\
\frac{\delta \rho^{(3)}}{\delta x} & \frac{\delta \rho^{(3)}}{\delta y} &
\frac{\delta \rho^{(3)}}{\delta z} & \frac{\delta \rho^{(3)}}{\delta b} \\
\vdots & \vdots & \vdots & \vdots \\
\frac{\delta \rho^{(k)}}{\delta x} & \frac{\delta \rho^{(k)}}{\delta y} &
\frac{\delta \rho^{(k)}}{\delta z} & \frac{\delta \rho^{(k)}}{\delta b}
\end{pmatrix}
\begin{pmatrix} \Delta x \\ \Delta y \\ \Delta z \\ \Delta b \end{pmatrix} +
\begin{pmatrix}
\epsilon^{(1)} \\
\epsilon^{(2)} \\
\epsilon^{(3)} \\
\vdots \\
\epsilon^{(k)} \\
\end{pmatrix}
$$

In matrix notation, this equation is commonly written

$$
\mathbf{b} = \mathbf{Ax} + \mathbf{\epsilon}
$$

where vectors are denoted with boldface lower case characters and matrices with boldface upper case. GNSS resources typically refer to the matrix $\mathbf{A}$ as the geometry matrix $\mathbf{G}$. We will follow this convention here. We will call the pseudorange residual matrix $\Delta \mathbf{\rho}$. It also improves clarity to separate the receiver position from the clock offset, so we will continue to denote the clock offset $b$ while writing the position $\Delta x, \Delta y, \Delta z$ in vector form $\Delta \mathbf{x}$. Thus our system of linear equations becomes,

$$
\Delta \mathbf{\rho} = \mathbf{G} \begin{pmatrix} \Delta \mathbf{x} \\ \Delta b \end{pmatrix} + \mathbf{\epsilon}
$$

It turns out the first three columns of $\mathbf{G}$ are just the unit vectors between the $k$th satellite and the estimated receiver position. This is because the unit vector effectively represents the proportion of the pseudorange which is tangent to each dimension. These unit vectors are found by normalizing the vectors $\mathbf{x}^{(k)} - \mathbf{\hat{x}}$ by their magnitudes $r^{(k)}$, where

$$
r^{(k)} = \lVert \mathbf{x}^{(k)} - \mathbf{\hat{x}} \rVert = \sqrt{(x^{(k)} - \hat{x})^2 + (y^{(k)} - \hat{y})^2 + (z^{(k)} - \hat{z})^2}
$$

All values in the fourth column equal 1, because the clock offset adds directly to the pseudorange with no geometric scaling factor required. Thus the geometry matrix G becomes:

$$
\mathbf{G} =
\begin{pmatrix}
\frac{x^{(1)} - \hat{x}}{r^{(1)}} & \frac{y^{(1)} - \hat{y}}{r^{(1)}} &
\frac{z^{(1)} - \hat{z}}{r^{(1)}} & 1 \\
\frac{x^{(2)} - \hat{x}}{r^{(2)}} & \frac{y^{(2)} - \hat{y}}{r^{(2)}} &
\frac{z^{(2)} - \hat{z}}{r^{(2)}} & 1 \\
\frac{x^{(3)} - \hat{x}}{r^{(3)}} & \frac{y^{(3)} - \hat{y}}{r^{(3)}} &
\frac{z^{(3)} - \hat{z}}{r^{(3)}} & 1 \\
\vdots & \vdots & \vdots & \vdots \\
\frac{x^{(k)} - \hat{x}}{r^{(k)}} & \frac{y^{(k)} - \hat{y}}{r^{(k)}} &
\frac{z^{(k)} - \hat{z}}{r^{(k)}} & 1 \\
\end{pmatrix}
$$

For a full derivation of these partial derivatives, refer to Misra and Enge, [Raquet (2013)](http://indico.ictp.it/event/a12180/session/21/contribution/12/material/0/0.pdf), or Ankur Mohan's excellent [blog post](https://www.telesens.co/2017/07/17/calculating-position-from-raw-gps-data/).

The matrix $\Delta \mathbf{\rho}$ is the difference between the measured and calculated pseudorange with the estimated parameters, calculated as follows:

$$
\Delta \mathbf{\rho} =
\begin{pmatrix} \Delta P^{(1)} \\
\Delta P^{(2)} \\
\Delta P^{(3)} \\
\vdots \\
\Delta P^{(k)}
\end{pmatrix}
=
\begin{pmatrix}
\rho^{(1)} - \hat{\rho}^{(1)} \\
\rho^{(2)} - \hat{\rho}^{(2)}  \\
\rho^{(3)} - \hat{\rho}^{(3)}  \\
\vdots \\
\rho^{(k)} -  \hat{\rho}^{(k)}  \\
\end{pmatrix}
=
\begin{pmatrix}
\rho^{(1)} - r^{(1)} - b \\
\rho^{(2)} - r^{(2)} - b \\
\rho^{(3)} - r^{(3)} - b \\
\vdots \\
\rho^{(k)} - r^{(k)} - b  \\
\end{pmatrix}
$$

Solving the linear system to minimise the sum of the squared residuals $\epsilon^{(k)}$ results in the solution to the normal equations,

$$
\begin{pmatrix} \Delta x \\ \Delta y \\ \Delta z \\ \Delta b \end{pmatrix}
=
\begin{pmatrix} \Delta \mathbf{x} \\ \Delta b \end{pmatrix}
=
\begin{pmatrix}
\mathbf{G}^T \mathbf{G}
\end{pmatrix}^{-1} \mathbf{G}^T \Delta \mathbf{\rho}
$$

See p. 17 of [Blewitt](http://www.nbmg.unr.edu/staff/pdfs/blewitt%20basics%20of%20gps.pdf) (1997) for a full derivation of this solution. After performing each linear least squares update, the estimated parameters $\mathbf{\hat{x}}, \mathbf{b}$ are updated:

$$
\begin{pmatrix}
\hat{x}_{i + 1} \\
\hat{y}_{i + 1} \\
\hat{z}_{i + 1} \\
\hat{b}_{i + 1} \\
\end{pmatrix}
=
\begin{pmatrix}
\hat{x}_{i} \\
\hat{y}_{i} \\
\hat{z}_{i} \\
\hat{b}_{i} \\
\end{pmatrix}
+
\begin{pmatrix} \Delta x \\ \Delta y \\ \Delta z \\ \Delta b \end{pmatrix}
$$

This process is performed iteratively until the magnitude of the change in position, $\lVert \Delta \mathbf{x} \rVert$, falls below a set threshold. In practice it usually only takes a few iterations for the solution to converge.

Below is the function to calculate rough receiver position. Its inputs are Numpy arrays containing satellite positions in the ECEF frame, pseudorange measurements in meters, and initial estimates of receiver position and clock bias. The unit for clock bias is meters, so you need to divide by the speed of light to convert to seconds.

This function is adapted and simplified from the MATLAB code in Ankur Mohan's [blog](https://www.telesens.co/2017/07/17/calculating-position-from-raw-gps-data/). It's a simple least squares iteration with no correction for ionospheric delay or satellite movement during signal time of flight.

```python
def least_squares(xs, measured_pseudorange, x0, b0):
    dx = 100*np.ones(3)
    b = b0
    # set up the G matrix with the right dimensions. We will later replace the first 3 columns
    # note that b here is the clock bias in meters equivalent, so the actual clock bias is b/LIGHTSPEED
    G = np.ones((measured_pseudorange.size, 4))
    iterations = 0
    while np.linalg.norm(dx) > 1e-3:
        r = np.linalg.norm(xs - x0, axis=1)
        phat = r + b0
        deltaP = measured_pseudorange - phat
        G[:, 0:3] = -(xs - x0) / r[:, None]
        sol = np.linalg.inv(np.transpose(G) @ G) @ np.transpose(G) @ deltaP
        dx = sol[0:3]
        db = sol[3]
        x0 = x0 + dx
        b0 = b0 + db
    norm_dp = np.linalg.norm(deltaP)
    return x0, b0, norm_dp

x, b, dp = least_squares(xs, pr, x0, b0)
print(navpy.ecef2lla(x))
print(b/LIGHTSPEED)
print(dp)
```

    (47.58626777321704, -122.32842161909167, -4.783203065395355)
    2.214374492481878e-07
    29.948493973072832

Looks like it worked! The pseudorange residual is in the tens of meters, which is reasonable considering we didn't correct for ionospheric delay or the rotation of the Earth during signal transmission.

Now let's wrap this entire process into a loop to calculate receiver position for every measurement epoch in the dataset with more than 4 satellite measurements.

```python
ecef_list = []
for epoch in measurements['Epoch'].unique():
    one_epoch = measurements.loc[(measurements['Epoch'] == epoch) & (measurements['prSeconds'] < 0.1)]
    one_epoch = one_epoch.drop_duplicates(subset='SvName').set_index('SvName')
    if len(one_epoch.index) > 4:
        timestamp = one_epoch.iloc[0]['UnixTime'].to_pydatetime(warn=False)
        sats = one_epoch.index.unique().tolist()
        ephemeris = manager.get_ephemeris(timestamp, sats)
        sv_position = calculate_satellite_position(ephemeris, one_epoch['tTxSeconds'])

        xs = sv_position[['x_k', 'y_k', 'z_k']].to_numpy()
        pr = one_epoch['PrM'] + LIGHTSPEED * sv_position['delT_sv']
        pr = pr.to_numpy()

        x, b, dp = least_squares(xs, pr, x, b)
        ecef_list.append(x)

ecef_array = np.stack(ecef_list, axis=0)
lla_array = np.stack(navpy.ecef2lla(ecef_array), axis=1)
ref_lla = lla_array[0, :]
ned_array = navpy.ecef2ned(ecef_array, ref_lla[0], ref_lla[1], ref_lla[2])

```

Finally, we'll convert the list of NED translations into a Pandas dataframe and create a plot of change in position relative to the first measurement epoch.

```python
import matplotlib.pyplot as plt
ned_df = pd.DataFrame(ned_array, columns=['N', 'E', 'D'])
plt.style.use('dark_background')
plt.plot(ned_df['E'], ned_df['N'])

# Add titles
plt.title('Position Offset From First Epoch')
plt.xlabel("East (m)")
plt.ylabel("North (m)")
plt.gca().set_aspect('equal', adjustable='box')
```


# 3. 1D Burgers Equation

We consider the 1d Burgers equation

$$
\partial_t u + u \partial_x u = \nu \frac{\partial ^2u}{\partial x^2}
$$

$u_0(x) := u(x,t)$ denotes the initial condition.
We choose homogeneous neumann boundary conditions in this example, i.e.
$$\partial_n u = 0, \partial \Omega$$ with $\Omega = (0,1)$

```python
# needed imports
from numpy import zeros, ones, linspace, zeros_like
from matplotlib.pyplot import plot, show
%matplotlib inline
```

```python
# Initial condition
from numpy import exp
#u0 = lambda x: exp(-(x-.3)**2/.05**2)
u0 = lambda x: exp(-(x-.5)**2/.02**2)

grid = linspace(0., 1., 401)
u = u0(grid)
```

```python
plot(grid, u) ; show()
```

## Time scheme
We shall use a $\theta$-scheme in this case and consider the following problem

$$
\frac{u^{n+1}-u^n}{\Delta t} +
\theta~ u^{n+1} \partial_x u^{n+1} + (1-\theta)~ u^n \partial_x u^n = \theta~\nu \frac{\partial ^2u^{n+1}}{\partial x^2} + (1-\theta)~\nu \frac{\partial ^2u^{n}}{\partial x^2}
$$

hence
$$
u^{n+1} + \Delta t ~ \theta~ u^{n+1} \partial_x u^{n+1} - \Delta t ~ \theta~\nu \frac{\partial ^2u^{n+1}}{\partial x^2} =
u^{n} - \Delta t ~ (1-\theta)~ u^{n} \partial_x u^{n} + \Delta t ~ (1-\theta)~\nu \frac{\partial ^2u^{n}}{\partial x^2}
$$
from now on, we shall denote by $f^n$ the right hand side of the previous equation
$$f^n := u^{n} - \Delta t ~ (1-\theta)~ u^{n} \partial_x u^{n} + \Delta t ~ (1-\theta)~\nu \frac{\partial ^2u^{n}}{\partial x^2}$$

## Weak formulation

Let $v \in \mathcal{V}$ be a function test, we have by integrating by parts the highest order term:

$$
\langle v, u^{n+1}  \rangle
+ \Delta t ~ \theta~ \langle v, u^{n+1} \partial_x u^{n+1}  \rangle
+ \Delta t ~ \theta~\nu \langle \frac{\partial v}{\partial x}, \frac{\partial u^{n+1}}{\partial x}  \rangle
=
\langle v, f^n \rangle
$$
The previous weak formulation is still nonlinear with respect to $u^{n+1}$. We shall then follow the same strategy as for the previous chapter on nonlinear Poisson problem.

The strategy is to define the left hand side as a **LinearForm** with respect to $v$, then linearize it around $u^{n+1}$. We therefor can use either Picard or Newton method to treat the nonlinearity.

We consider the following linear form

$$
G(v;u,w) := \langle v, u  \rangle
+ \Delta t ~ \theta~ \langle v, w \partial_x u  \rangle
+ \Delta t ~ \theta~\nu \langle \frac{\partial v}{\partial x}, \frac{\partial u}{\partial x}  \rangle
, \quad \forall u,v,w \in \mathcal{V}
$$

Our problem is then

$$
\mbox{Find } u^{n+1} \in \mathcal{V}, \mbox{such that}\\
G(v;u^{n+1},u^{n+1}) = l(v), \quad \forall v \in \mathcal{V}
$$

where

$$
l(v) := \int_{\Omega} f^n v ~d\Omega, \quad \forall v \in \mathcal{V}
$$

## Abstract Model

```python
from sympde.core import Constant
from sympde.expr import BilinearForm, LinearForm, integral
from sympde.topology import ScalarFunctionSpace, Line, element_of
from sympde.topology import dx1 as dx # TODO: this is a bug right now
from sympde.expr     import find
from sympde.expr.expr import linearize

from psydac.fem.basic          import FemField
from psydac.api.discretization import discretize
```

```python
domain = Line()

V = ScalarFunctionSpace('V', domain)
u  = element_of(V, name='u')
v  = element_of(V, name='v')
w  = element_of(V, name='w')
un  = element_of(V, name='un') # time iteration
uk  = element_of(V, name='uk') # nonlinear solver iteration

x = domain.coordinates

nu = Constant('nu')
theta = Constant('theta')
dt = Constant('dt')
```

#### Defining the Linear form $G$

```python
# Linear form g: V --> R
expr = v * u + dt*theta*v*w*dx(u) + dt*theta*nu*dx(v)*dx(u)
g = LinearForm(v, integral(domain, expr))
```

#### Defining the Linear form $l$

```python
# Linear form l: V --> R
expr = v * un - dt*theta*v*un*dx(un) - dt*theta*nu*dx(v)*dx(un)
l = LinearForm(v, integral(domain, expr))
```

### Picard Method

$$
\mbox{Find } u_{n+1} \in \mathcal{V}_h, \mbox{such that}\\
G(v;u_{n+1},u_n) = l(v), \quad \forall v \in \mathcal{V}_h
$$

#### Picard iteration

```python
# Variational problem
picard = find(u, forall=v, lhs=g(v, u=u,w=uk), rhs=l(v))
```

### Newton Method

Let's define
$$
F(v;u) := G(v;u,u) -l(v), \quad \forall v \in \mathcal{V}
$$

Newton method writes
$$
\mbox{Find } u_{n+1} \in \mathcal{V}_h, \mbox{such that}\\
F^{\prime}(\delta u,v; u_n) = - F(v;u_n), \quad \forall v \in \mathcal{V} \\
u_{n+1} := u_{n} + \delta u, \quad \delta u \in \mathcal{V}
$$

#### Newton Iteration

```python
F = LinearForm(v, g(v,w=u)-l(v))
du  = element_of(V, name='du')

Fprime = linearize(F, u, trials=du)

# Variational problem
newton = find(du, forall=v, lhs=Fprime(du, v,u=uk), rhs=-F(v,u=uk))
```

## Discrete Space

```python
# Create computational domain from topological domain
domain_h = discretize(domain, ncells=[64], comm=None)

# Discrete spaces
Vh = discretize(V, domain_h, degree=[2])
```

### Picard method

```python
# Discretize equation
picard_h = discretize(picard, domain_h, [Vh, Vh])

def picard(Un, niter=10):
    Uk = FemField( Vh, Vh.vector_space.zeros() )

    for i in range(niter):
        Uk  = picard_h.solve(uk=Uk, un=Un)

    return Uk
```

### Newton method

```python
# Discretize equation
newton_h = discretize(newton, domain_h, [Vh, Vh])

def newton(Un, niter=10):
    Uk = FemField( Vh, Vh.vector_space.zeros() )

    for i in range(niter):
        delta_x  = newton_h.solve(uk=Uk, un=Un)
        Uk = FemField( Vh, delta_x.coeffs + Uk.coeffs )

    return Uk
```

## L2-Projection

```python
a_mass = BilinearForm((u,v), integral(domain , u*v))

from sympy import exp
#u0 = exp(-(x-.3)**2/.05**2)
u0 = exp(-(x-.5)**2/.02**2)

l_mass = LinearForm(v, integral(domain, u0*v))

# Abstract projection
projection = find(u, forall=v, lhs=a_mass(u, v), rhs=l_mass(v))

# Discrete projection
projection_h = discretize(projection, domain_h, [Vh, Vh])
```

```python
u0_h = projection_h.solve()
```

```python
from utilities.plot import plot_field_1d
plot_field_1d(Vh.knots[0], Vh.degree[0], u0_h.coeffs.toarray(), nx=401)
```

### Important note

Right now, Psydac does not provide a **grmes** solver. For this reason, we shall rewrite our nonlinear solver using the following **gmres_driver**

```python
from scipy.sparse.linalg import gmres
def gmres_driver(equation_h, tol=1.e-8, maxiter=5000):
    M   = equation_h.linear_system.lhs.tosparse()
    rhs = equation_h.linear_system.rhs.toarray()

    x, status = gmres(M, rhs, tol=tol, maxiter=maxiter)

    xh = FemField( Vh, Vh.vector_space.zeros() )
    n = xh.coeffs._data.shape[0]
    xh.coeffs._data[Vh.degree[0]:n-Vh.degree[0]] = x[:]

    return xh
```

```python
def picard(Un, theta, nu, dt, niter=10):
    Uk = FemField( Vh, Vh.vector_space.zeros() )

    for i in range(niter):
        picard_h.assemble(uk=Uk, un=Un, theta=theta, nu=nu, dt=dt)
        Uk = gmres_driver(picard_h, tol=1.e-8, maxiter=5000)

    return Uk
```

```python
def newton(Un, theta, nu, dt, niter=10):
    Uk = FemField( Vh, Vh.vector_space.zeros() )

    for i in range(niter):
        newton_h.assemble(uk=Uk, un=Un, theta=theta, nu=nu, dt=dt)
        delta_x = gmres_driver(newton_h, tol=1.e-8, maxiter=5000)
        Uk = FemField( Vh, delta_x.coeffs + Uk.coeffs )

    return Uk
```

```python
Uk = picard(u0_h, theta=0.5, nu=0.5, dt=0.01)
```

```python
plot_field_1d(Vh.knots[0], Vh.degree[0], Uk.coeffs.toarray(), nx=401)
```

```python
Uk = newton(u0_h, theta=0.5, nu=0.5, dt=0.01)
plot_field_1d(Vh.knots[0], Vh.degree[0], Uk.coeffs.toarray(), nx=401)
```

```python
def time_solver(theta, nu, dt, T, nonlinear_solver, U0):
    n_time = int(T / dt)
    Uk = U0.copy()
    for i_time in range(0, n_time):
        Uk = nonlinear_solver(Uk, theta=theta, nu=nu, dt=dt)
    return Uk
```

```python
Uk = time_solver(theta=0.5, nu=0.02, dt=0.01, T=0.1, nonlinear_solver=newton, U0=u0_h)
```

```python
plot_field_1d(Vh.knots[0], Vh.degree[0], Uk.coeffs.toarray(), nx=401)
```

```python

```

```python

```


```python
import math
from collections import defaultdict
import matplotlib.pyplot as plt
from chempy import ReactionSystem
from chempy.kinetics.ode import get_odesys
from chempy.kinetics.rates import SinTemp
%matplotlib inline
```

```python
rsys = ReactionSystem.from_string("""
2 HNO2 -> H2O + NO + NO2; MassAction(EyringHS.fk('dH1', 'dS1'))
2 NO2 -> N2O4; MassAction(EyringHS.fk('dH2', 'dS2'))
""")  # fictitious thermodynamic parameters
```

```python
st = SinTemp(unique_keys='Tbase Tamp Tangvel Tphase'.split())
```

```python
class Constants:
    Planck_constant = 6.63e-34
    Boltzmann_constant = 1.38e-23
    molar_gas_constant = 8.314

odesys, extra = get_odesys(rsys, include_params=False, substitutions={'temperature': st}, constants=Constants())
```

```python
init_conc = defaultdict(lambda: 0, HNO2=1, H2O=55)
params = dict(
    Tbase=300,
    Tamp=10,
    Tangvel=2*math.pi/10,
    Tphase=-math.pi/2,
    dH1=85e3,
    dS1=10,
    dH2=70e3,
    dS2=20
)
duration = 60
```

```python
def integrate_and_plot(system):
    result = system.integrate(duration, init_conc, params, integrator='cvode', nsteps=2000)
    fig, axes = plt.subplots(1, 2, figsize=(14, 4))
    result.plot(names='NO HNO2 N2O4'.split(), ax=axes[0])
    result.plot(names='NO2'.split(), ax=axes[1])
    print({k: v for k, v in sorted(result.info.items()) if not k.startswith('internal')})
```

```python
integrate_and_plot(odesys)
```

```python
odesys.param_names
```

```python
len(odesys.exprs)
```

```python
asys = odesys.as_autonomous()
len(asys.exprs)
```

```python
[a - o for a, o in zip(asys.exprs[:-1],odesys.exprs)]
```

```python
asys.exprs[-1]
```

```python
asys.get_jac()[:-1,:-1] - odesys.get_jac()
```

```python
import sympy as sym
sym.init_printing()
args = _x, _y, _p = asys.pre_process(*asys.to_arrays(1, init_conc, params))
args
```

```python
asys.f_cb(*args)
```

```python
asys.j_cb(*args)
```

```python
argsode = odesys.pre_process(*odesys.to_arrays(1, init_conc, params))
argsode
```

```python
argsode[0] - args[0], argsode[1] - args[1][:-1], argsode[2] - args[2]
```

```python
odesys.f_cb(*argsode)
```

```python
odesys.j_cb(*argsode)
```

```python
integrate_and_plot(asys)
```

```python
odesys.ny, asys.ny
```

```python
asys.pre_process(1, [0,1,2,3,4], [5,6,7,8])
```

```python

```