# PHY321: Two-body problems and Gravitational Forces

**[Morten Hjorth-Jensen](http://mhjgit.github.io/info/doc/web/)**, Department of Physics and Astronomy and Facility for Rare Ion Beams (FRIB), Michigan State University, USA and Department of Physics, University of Oslo, Norway

Date: **Mar 17, 2021**

Copyright 1999-2021, [Morten Hjorth-Jensen](http://mhjgit.github.io/info/doc/web/). Released under CC Attribution-NonCommercial 4.0 license

## Aims and Overarching Motivation

### Monday

Definition of the two-body problem, rewriting the equations in relative and center-of-mass coordinates

**Reading suggestion**: Taylor sections 8.2-8.3

### Wednesday

The gravitional force and its solution in two dimensions

**Reading suggestion**: Taylor chapter 8.4

### Friday

Kepler's laws

**Reading suggestion**: Taylor section 8.5-8.6

# Two-body Problems

The gravitational potential energy and forces involving two masses $a$ and $b$ are

$$
\begin{eqnarray}
V_{ab}&=&-\frac{Gm_am_b}{|\boldsymbol{r}_a-\boldsymbol{r}_b|},\\
\nonumber
F_{ba}&=&-\frac{Gm_am_b}{|\boldsymbol{r}_a-\boldsymbol{r}_b|^2}\hat{r}_{ab},\\
\nonumber
\hat{r}_{ab}&=&\frac{\boldsymbol{r}_b-\boldsymbol{r}_a}{|\boldsymbol{r}_a-\boldsymbol{r}_b|}.
\end{eqnarray}
$$

Here $G=6.67\times 10^{-11}$ Nm$^2$/kg$^2$, and $F_{ba}$ is the force
on $b$ due to $a$. By inspection, one can see that the force on $b$
due to $a$ and the force on $a$ due to $b$ are equal and opposite. The
net potential energy for a large number of masses would be

$$
\begin{equation}
V=\sum_{a<b}U_{ab}=\frac{1}{2}\sum_{a\ne b}V_{ab}.
\label{_auto1} \tag{1}
\end{equation}
$$

## Relative and Center of Mass Motion

Thus far, we have considered the trajectory as if the force is
centered around a fixed point. For two bodies interacting only with
one another, both masses circulate around the center of mass. One
might think that solutions would become more complex when both
particles move, but we will see here that the problem can be reduced
to one with a single body moving according to a fixed force by
expressing the trajectories for $\boldsymbol{r}_1$ and $\boldsymbol{r}_2$ into the
center-of-mass coordinate $\boldsymbol{R}_{\rm cm}$ and the relative
coordinate $\boldsymbol{r}$,

$$
\begin{eqnarray}
\boldsymbol{R}_{\rm cm}&\equiv&\frac{m_1\boldsymbol{r}_1+m_2\boldsymbol{r}_2}{m_1+m_2},\\
\nonumber
\boldsymbol{r}&\equiv&\boldsymbol{r}_1-\boldsymbol{r_2}.
\end{eqnarray}
$$

Here, we assume the two particles interact only with one another, so
$\boldsymbol{F}_{12}=-\boldsymbol{F}_{21}$ (where $\boldsymbol{F}_{ij}$ is the force on $i$
due to $j$. The equations of motion then become

$$
\begin{eqnarray}
\ddot{\boldsymbol{R}}_{\rm cm}&=&\frac{1}{m_1+m_2}\left\{m_1\ddot{\boldsymbol{r}}_1+m_2\ddot{\boldsymbol{r}}_2\right\}\\
\nonumber
&=&\frac{1}{m_1+m_2}\left\{\boldsymbol{F}_{12}+\boldsymbol{F}_{21}\right\}=0.\\
\ddot{\boldsymbol{r}}&=&\ddot{\boldsymbol{r}}_1-\ddot{\boldsymbol{r}}_2=\left(\frac{\boldsymbol{F}_{12}}{m_1}-\frac{\boldsymbol{F}_{21}}{m_2}\right)\\
\nonumber
&=&\left(\frac{1}{m_1}+\frac{1}{m_2}\right)\boldsymbol{F}_{12}.
\end{eqnarray}
$$

The first expression simply states that the center of mass coordinate
$\boldsymbol{R}_{\rm cm}$ moves at a fixed velocity. The second expression
can be rewritten in terms of the reduced mass $\mu$.

$$
\begin{eqnarray}
\mu \ddot{\boldsymbol{r}}&=&\boldsymbol{F}_{12},\\
\frac{1}{\mu}&=&\frac{1}{m_1}+\frac{1}{m_2},~~~~\mu=\frac{m_1m_2}{m_1+m_2}.
\end{eqnarray}
$$

Thus, one can treat the trajectory as a one-body problem where the
reduced mass is $\mu$, and a second trivial problem for the center of
mass. The reduced mass is especially convenient when one is
considering gravitational problems because then

$$
\begin{eqnarray}
\mu \ddot{r}&=&-\frac{Gm_1m_2}{r^2}\hat{r}\\
\nonumber
&=&-\frac{GM\mu}{r^2}\hat{r},~~~M\equiv m_1+m_2.
\end{eqnarray}
$$

For the gravitational problem, the reduced mass then falls out and the
trajectory depends only on the total mass $M$.

The kinetic energy and momenta also have analogues in center-of-mass
coordinates. The total and relative momenta are

$$
\begin{eqnarray}
\boldsymbol{P}&\equiv&\boldsymbol{p}_1+\boldsymbol{p}_2=M\dot{\boldsymbol{R}}_{\rm cm},\\
\nonumber
\boldsymbol{q}&\equiv&\mu\dot{\boldsymbol{r}}.
\end{eqnarray}
$$

With these definitions, a little algebra shows that the kinetic energy becomes

$$
\begin{eqnarray}
T&=&\frac{1}{2}m_1|\boldsymbol{v}_1|^2+\frac{1}{2}m_2|\boldsymbol{v}_2|^2\\
\nonumber
&=&\frac{1}{2}M|\dot{\boldsymbol{R}}_{\rm cm}|^2
+\frac{1}{2}\mu|\dot{\boldsymbol{r}}|^2\\
\nonumber
&=&\frac{P^2}{2M}+\frac{q^2}{2\mu}.
\end{eqnarray}
$$

The standard strategy is to transform into the center of mass frame,
then treat the problem as one of a single particle of mass $\mu$
undergoing a force $\boldsymbol{F}_{12}$. Scattering angles can also be
expressed in this frame, then transformed into the lab frame. In
practice, one sees examples in the literature where $d\sigma/d\Omega$
expressed in both the "center-of-mass" and in the "laboratory"
frame.

## Deriving Elliptical Orbits

Kepler's laws state that a gravitational orbit should be an ellipse
with the source of the gravitational field at one focus. Deriving this
is surprisingly messy. To do this, we first use angular momentum
conservation to transform the equations of motion so that it is in
terms of $r$ and $\theta$ instead of $r$ and $t$. The overall strategy
is to

1. Find equations of motion for $r$ and $t$ with no angle ($\theta$) mentioned, i.e. $d^2r/dt^2=\cdots$. Angular momentum conservation will be used, and the equation will involve the angular momentum $L$.

2. Use angular momentum conservation to find an expression for $\dot{\theta}$ in terms of $r$.

3. Use the chain rule to convert the equations of motions for $r$, an expression involving $r,\dot{r}$ and $\ddot{r}$, to one involving $r,dr/d\theta$ and $d^2r/d\theta^2$. This is quitecomplicated because the expressions will also involve a substitution $u=1/r$ so that one finds an expression in terms of $u$ and $\theta$.

4. Once $u(\theta)$ is found, you need to show that this can be converted to the familiar form for an ellipse.

The equations of motion give

$$
\begin{eqnarray}
\label{eq:radialeqofmotion} \tag{2}
\frac{d}{dt}r^2&=&\frac{d}{dt}(x^2+y^2)=2x\dot{x}+2y\dot{y}=2r\dot{r},\\
\nonumber
\dot{r}&=&\frac{x}{r}\dot{x}+\frac{y}{r}\dot{y},\\
\nonumber
\ddot{r}&=&\frac{x}{r}\ddot{x}+\frac{y}{r}\ddot{y}
+\frac{\dot{x}^2+\dot{y}^2}{r}
-\frac{\dot{r}^2}{r}.
\end{eqnarray}
$$

Recognizing that the numerator of the third term is the velocity squared, and that it can be written in polar coordinates,

$$
\begin{equation}
v^2=\dot{x}^2+\dot{y}^2=\dot{r}^2+r^2\dot{\theta}^2,
\label{_auto2} \tag{3}
\end{equation}
$$

one can write $\ddot{r}$ as

$$
\begin{eqnarray}
\label{eq:radialeqofmotion2} \tag{4}
\ddot{r}&=&\frac{F_x\cos\theta+F_y\sin\theta}{m}+\frac{\dot{r}^2+r^2\dot{\theta}^2}{r}-\frac{\dot{r}^2}{r}\\
\nonumber
&=&\frac{F}{m}+\frac{r^2\dot{\theta}^2}{r}\\
\nonumber
m\ddot{r}&=&F+\frac{L^2}{mr^3}.
\end{eqnarray}
$$

This derivation used the fact that the force was radial,
$F=F_r=F_x\cos\theta+F_y\sin\theta$, and that angular momentum is
$L=mrv_{\theta}=mr^2\dot{\theta}$. The term $L^2/mr^3=mv^2/r$ behaves
like an additional force. Sometimes this is referred to as a
centrifugal force, but it is not a force. Instead, it is the
consequence of considering the motion in a rotating (and therefore
accelerating) frame.

Now, we switch to the particular case of an attractive inverse square
force, $F=-\alpha/r^2$, and show that the trajectory, $r(\theta)$, is
an ellipse. To do this we transform derivatives w.r.t. time to
derivatives w.r.t. $\theta$ using the chain rule combined with angular
momentum conservation, $\dot{\theta}=L/mr^2$.

$$
\begin{eqnarray}
\label{eq:rtotheta} \tag{5}
\dot{r}&=&\frac{dr}{d\theta}\dot{\theta}=\frac{dr}{d\theta}\frac{L}{mr^2},\\
\nonumber
\ddot{r}&=&\frac{d^2r}{d\theta^2}\dot{\theta}^2
+\frac{dr}{d\theta}\left(\frac{d}{dr}\frac{L}{mr^2}\right)\dot{r}\\
\nonumber
&=&\frac{d^2r}{d\theta^2}\left(\frac{L}{mr^2}\right)^2
-2\frac{dr}{d\theta}\frac{L}{mr^3}\dot{r}\\
\nonumber
&=&\frac{d^2r}{d\theta^2}\left(\frac{L}{mr^2}\right)^2
-\frac{2}{r}\left(\frac{dr}{d\theta}\right)^2\left(\frac{L}{mr^2}\right)^2
\end{eqnarray}
$$

Equating the two expressions for $\ddot{r}$ in Eq.s ([4](#eq:radialeqofmotion2)) and ([5](#eq:rtotheta)) eliminates all the derivatives w.r.t. time, and provides a differential equation with only derivatives w.r.t. $\theta$,

$$
\begin{equation}
\label{eq:rdotdot} \tag{6}
\frac{d^2r}{d\theta^2}\left(\frac{L}{mr^2}\right)^2
-\frac{2}{r}\left(\frac{dr}{d\theta}\right)^2\left(\frac{L}{mr^2}\right)^2
=\frac{F}{m}+\frac{L^2}{m^2r^3},
\end{equation}
$$

that when solved yields the trajectory, i.e. $r(\theta)$. Up to this
point the expressions work for any radial force, not just forces that
fall as $1/r^2$.

The trick to simplifying this differential equation for the inverse
square problems is to make a substitution, $u\equiv 1/r$, and rewrite
the differential equation for $u(\theta)$.

$$
\begin{eqnarray}
r&=&1/u,\\
\nonumber
\frac{dr}{d\theta}&=&-\frac{1}{u^2}\frac{du}{d\theta},\\
\nonumber
\frac{d^2r}{d\theta^2}&=&\frac{2}{u^3}\left(\frac{du}{d\theta}\right)^2-\frac{1}{u^2}\frac{d^2u}{d\theta^2}.
\end{eqnarray}
$$

Plugging these expressions into Eq. ([6](#eq:rdotdot)) gives an
expression in terms of $u$, $du/d\theta$, and $d^2u/d\theta^2$. After
some tedious algebra,

$$
\begin{equation}
\frac{d^2u}{d\theta^2}=-u-\frac{F m}{L^2u^2}.
\label{_auto3} \tag{7}
\end{equation}
$$

For the attractive inverse square law force, $F=-\alpha u^2$,

$$
\begin{equation}
\frac{d^2u}{d\theta^2}=-u+\frac{m\alpha}{L^2}.
\label{_auto4} \tag{8}
\end{equation}
$$

The solution has two arbitrary constants, $A$ and $\theta_0$,

$$
\begin{eqnarray}
\label{eq:Ctrajectory} \tag{9}
u&=&\frac{m\alpha}{L^2}+A\cos(\theta-\theta_0),\\
\nonumber
r&=&\frac{1}{(m\alpha/L^2)+A\cos(\theta-\theta_0)}.
\end{eqnarray}
$$

The radius will be at a minimum when $\theta=\theta_0$ and at a
maximum when $\theta=\theta_0+\pi$. The constant $A$ is related to the
eccentricity of the orbit. When $A=0$ the radius is a constant
$r=L^2/(m\alpha)$, and the motion is circular. If one solved the
expression $mv^2/r=-\alpha/r^2$ for a circular orbit, using the
substitution $v=L/(mr)$, one would reproduce the expression
$r=L^2/(m\alpha)$.

The form describing the elliptical trajectory in
Eq. ([9](#eq:Ctrajectory)) can be identified as an ellipse with one
focus being the center of the ellipse by considering the definition of
an ellipse as being the points such that the sum of the two distances
between the two foci are a constant. Making that distance $2D$, the
distance between the two foci as $2a$, and putting one focus at the
origin,

$$
\begin{eqnarray}
2D&=&r+\sqrt{(r\cos\theta-2a)^2+r^2\sin^2\theta},\\
\nonumber
4D^2+r^2-4Dr&=&r^2+4a^2-4ar\cos\theta,\\
\nonumber
r&=&\frac{D^2-a^2}{D+a\cos\theta}=\frac{1}{D/(D^2-a^2)-a\cos\theta/(D^2-a^2)}.
\end{eqnarray}
$$

By inspection, this is the same form as Eq. ([9](#eq:Ctrajectory)) with $D/(D^2-a^2)=m\alpha/L^2$ and $a/(D^2-a^2)=A$.

Let us remind ourselves about what an ellipse is before we proceed.

```python
%matplotlib inline

import numpy as np
from matplotlib import pyplot as plt
from math import pi

u=1.     #x-position of the center
v=0.5    #y-position of the center
a=2.     #radius on the x-axis
b=1.5    #radius on the y-axis

t = np.linspace(0, 2*pi, 100)
plt.plot( u+a*np.cos(t) , v+b*np.sin(t) )
plt.grid(color='lightgray',linestyle='--')
plt.show()
```

## Effective or Centrifugal Potential

The total energy of a particle is

$$
\begin{eqnarray}
E&=&U(r)+\frac{1}{2}mv_\theta^2+\frac{1}{2}m\dot{r}^2\\
\nonumber
&=&U(r)+\frac{1}{2}mr^2\dot{\theta}^2+\frac{1}{2}m\dot{r}^2\\
\nonumber
&=&U(r)+\frac{L^2}{2mr^2}+\frac{1}{2}m\dot{r}^2.
\end{eqnarray}
$$

The second term then contributes to the energy like an additional
repulsive potential. The term is sometimes referred to as the
"centrifugal" potential, even though it is actually the kinetic energy
of the angular motion. Combined with $U(r)$, it is sometimes referred
to as the "effective" potential,

$$
\begin{eqnarray}
U_{\rm eff}(r)&=&U(r)+\frac{L^2}{2mr^2}.
\end{eqnarray}
$$

Note that if one treats the effective potential like a real potential, one would expect to be able to generate an effective force,

$$
\begin{eqnarray}
F_{\rm eff}&=&-\frac{d}{dr}U(r) -\frac{d}{dr}\frac{L^2}{2mr^2}\\
\nonumber
&=&F(r)+\frac{L^2}{mr^3}=F(r)+m\frac{v_\perp^2}{r},
\end{eqnarray}
$$

which is indeed matches the form for $m\ddot{r}$ in Eq. ([4](#eq:radialeqofmotion2)), which included the **centrifugal** force.

The following code plots this effective potential for a simple choice of parameters, with a standard gravitational potential $-\alpha/r$. Here we have chosen $L=m=\alpha=1$.

```python
# Common imports
import numpy as np
from math import *
import matplotlib.pyplot as plt

Deltax = 0.01
#set up arrays
xinitial = 0.3
xfinal = 5.0
alpha = 1.0   # spring constant
m = 1.0   # mass, you can change these
AngMom = 1.0  #  The angular momentum
n = ceil((xfinal-xinitial)/Deltax)
x = np.zeros(n)
for i in range(n):
    x[i] = xinitial+i*Deltax
V = np.zeros(n)
V = -alpha/x+0.5*AngMom*AngMom/(m*x*x)
# Plot potential
fig, ax = plt.subplots()
ax.set_xlabel('r[m]')
ax.set_ylabel('V[J]')
ax.plot(x, V)
fig.tight_layout()
plt.show()
```

### Gravitational force example

Using the above parameters, we can now study the evolution of the system using for example the velocity Verlet method.
This is done in the code here for an initial radius equal to the minimum of the potential well.  We seen then that the radius is always the same and corresponds to a circle (the radius is always constant).

```python
# Common imports
import numpy as np
import pandas as pd
from math import *
import matplotlib.pyplot as plt
import os

# Where to save the figures and data files
PROJECT_ROOT_DIR = "Results"
FIGURE_ID = "Results/FigureFiles"
DATA_ID = "DataFiles/"

if not os.path.exists(PROJECT_ROOT_DIR):
    os.mkdir(PROJECT_ROOT_DIR)

if not os.path.exists(FIGURE_ID):
    os.makedirs(FIGURE_ID)

if not os.path.exists(DATA_ID):
    os.makedirs(DATA_ID)

def image_path(fig_id):
    return os.path.join(FIGURE_ID, fig_id)

def data_path(dat_id):
    return os.path.join(DATA_ID, dat_id)

def save_fig(fig_id):
    plt.savefig(image_path(fig_id) + ".png", format='png')

# Simple Gravitational Force   -alpha/r

DeltaT = 0.01
#set up arrays
tfinal = 100.0
n = ceil(tfinal/DeltaT)
# set up arrays for t, v and r
t = np.zeros(n)
v = np.zeros(n)
r = np.zeros(n)
# Constants of the model, setting all variables to one for simplicity
alpha = 1.0
AngMom = 1.0  #  The angular momentum
m = 1.0  # scale mass to one
c1 = AngMom*AngMom/(m*m)
c2 = AngMom*AngMom/m
rmin = (AngMom*AngMom/m/alpha)
# Initial conditions
r0 = rmin
v0 = 0.0
r[0] = r0
v[0] = v0
# Start integrating using the Velocity-Verlet  method
for i in range(n-1):
    # Set up acceleration
    a = -alpha/(r[i]**2)+c1/(r[i]**3)
    # update velocity, time and position using the Velocity-Verlet method
    r[i+1] = r[i] + DeltaT*v[i]+0.5*(DeltaT**2)*a
    anew = -alpha/(r[i+1]**2)+c1/(r[i+1]**3)
    v[i+1] = v[i] + 0.5*DeltaT*(a+anew)
    t[i+1] = t[i] + DeltaT
    # Plot position as function of time
fig, ax = plt.subplots(2,1)
ax[0].set_xlabel('time')
ax[0].set_ylabel('radius')
ax[0].plot(t,r)
ax[1].set_xlabel('time')
ax[1].set_ylabel('Velocity')
ax[1].plot(t,v)
save_fig("RadialGVV")
plt.show()
```

Changing the value of the initial position to a value where the energy is positive, leads to an increasing radius with time, a so-called unbound orbit. Choosing on the other hand an initial radius that corresponds to a negative energy and different from the minimum value leads to a radius that oscillates back and forth between two values.

### Harmonic Oscillator in two dimensions

Consider a particle of mass $m$ in a 2-dimensional harmonic oscillator with potential

$$
U=\frac{1}{2}kr^2=\frac{1}{2}k(x^2+y^2).
$$

If the orbit has angular momentum $L$, we can find the radius and angular velocity of the circular orbit as well as the b) the angular frequency of small radial perturbations.

We consider the effective potential. The radius of a circular orbit is at the minimum of the potential (where the effective force is zero).
The potential is plotted here with the parameters $k=m=0.1$ and $L=1.0$.

```python
# Common imports
import numpy as np
from math import *
import matplotlib.pyplot as plt

Deltax = 0.01
#set up arrays
xinitial = 1.0
xfinal = 5.0
k = 0.1   # spring constant
m = 0.1   # mass, you can change these
AngMom = 1.0  #  The angular momentum
n = ceil((xfinal-xinitial)/Deltax)
x = np.zeros(n)
for i in range(n):
    x[i] = xinitial+i*Deltax
V = np.zeros(n)
V = 0.5*k*x*x+0.5*AngMom*AngMom/(m*x*x)
# Plot potential
fig, ax = plt.subplots()
ax.set_xlabel('r[m]')
ax.set_ylabel('V[J]')
ax.plot(x, V)
fig.tight_layout()
plt.show()
```

$$
\begin{eqnarray*}
U_{\rm eff}&=&\frac{1}{2}kr^2+\frac{L^2}{2mr^2}
\end{eqnarray*}
$$

The effective potential looks like that of a harmonic oscillator for
large $r$, but for small $r$, the centrifugal potential repels the
particle from the origin. The combination of the two potentials has a
minimum for at some radius $r_{\rm min}$.

$$
\begin{eqnarray*}
0&=&kr_{\rm min}-\frac{L^2}{mr_{\rm min}^3},\\
r_{\rm min}&=&\left(\frac{L^2}{mk}\right)^{1/4},\\
\dot{\theta}&=&\frac{L}{mr_{\rm min}^2}=\sqrt{k/m}.
\end{eqnarray*}
$$

For particles at $r_{\rm min}$ with $\dot{r}=0$, the particle does not
accelerate and $r$ stays constant, i.e. a circular orbit. The radius
of the circular orbit can be adjusted by changing the angular momentum
$L$.

For the above parameters this minimum is at $r_{\rm min}=1$.

 Now consider small vibrations about $r_{\rm min}$. The effective spring constant is the curvature of the effective potential.

$$
\begin{eqnarray*}
k_{\rm eff}&=&\left.\frac{d^2}{dr^2}U_{\rm eff}(r)\right|_{r=r_{\rm min}}=k+\frac{3L^2}{mr_{\rm min}^4}\\
&=&4k,\\
\omega&=&\sqrt{k_{\rm eff}/m}=2\sqrt{k/m}=2\dot{\theta}.
\end{eqnarray*}
$$

Here, the second step used the result of the last step from part
(a). Because the radius oscillates with twice the angular frequency,
the orbit has two places where $r$ reaches a minimum in one
cycle. This differs from the inverse-square force where there is one
minimum in an orbit. One can show that the orbit for the harmonic
oscillator is also elliptical, but in this case the center of the
potential is at the center of the ellipse, not at one of the foci.

The solution is also simple to write down exactly in Cartesian coordinates. The $x$ and $y$ equations of motion separate,

$$
\begin{eqnarray*}
\ddot{x}&=&-kx,\\
\ddot{y}&=&-ky.
\end{eqnarray*}
$$

So the general solution can be expressed as

$$
\begin{eqnarray*}
x&=&A\cos\omega_0 t+B\sin\omega_0 t,\\
y&=&C\cos\omega_0 t+D\sin\omega_0 t.
\end{eqnarray*}
$$

The code here finds the solution for $x$ and $y$ using the code we developed in homework 4.

```python

DeltaT = 0.01
#set up arrays
tfinal = 10.0
n = ceil(tfinal/DeltaT)
# set up arrays
t = np.zeros(n)
v = np.zeros((n,2))
r = np.zeros((n,2))
radius = np.zeros(n)
# Constants of the model
k = 0.1   # spring constant
m = 0.1   # mass, you can change these
omega02 = sqrt(k/m)  # Frequency
AngMom = 1.0  #  The angular momentum
rmin = (AngMom*AngMom/k/m)**0.25
# Initial conditions as compact 2-dimensional arrays
#x0 =rmin*0.5; y0 = sqrt(rmin*rmin-x0*x0)
x0 = 1.0; y0= 1.0
r0 = np.array([x0,y0])
v0 = np.array([0.0,0.0])
r[0] = r0
v[0] = v0
# Start integrating using the Velocity-Verlet  method
for i in range(n-1):
    # Set up the acceleration
    a =  -r[i]*omega02
    # update velocity, time and position using the Velocity-Verlet method
    r[i+1] = r[i] + DeltaT*v[i]+0.5*(DeltaT**2)*a
    anew = -r[i+1]*omega02
    v[i+1] = v[i] + 0.5*DeltaT*(a+anew)
    t[i+1] = t[i] + DeltaT
# Plot position as function of time
radius = np.sqrt(r[:,0]**2+r[:,1]**2)
fig, ax = plt.subplots(3,1)
ax[0].set_xlabel('time')
ax[0].set_ylabel('radius squared')
ax[0].plot(t,r[:,0]**2+r[:,1]**2)
ax[1].set_xlabel('time')
ax[1].set_ylabel('x position')
ax[1].plot(t,r[:,0])
ax[2].set_xlabel('time')
ax[2].set_ylabel('y position')
ax[2].plot(t,r[:,1])

fig.tight_layout()
save_fig("2DimHOVV")
plt.show()
```

With some work using double angle formulas, one can calculate

$$
\begin{eqnarray*}
r^2&=&x^2+y^2\\
\nonumber
&=&(A^2+C^2)\cos^2(\omega_0t)+(B^2+D^2)\sin^2\omega_0t+(AB+CD)\cos(\omega_0t)\sin(\omega_0t)\\
\nonumber
&=&\alpha+\beta\cos 2\omega_0 t+\gamma\sin 2\omega_0 t,\\
\alpha&=&\frac{A^2+B^2+C^2+D^2}{2},~~\beta=\frac{A^2-B^2+C^2-D^2}{2},~~\gamma=AB+CD,\\
r^2&=&\alpha+(\beta^2+\gamma^2)^{1/2}\cos(2\omega_0 t-\delta),~~~\delta=\arctan(\gamma/\beta),
\end{eqnarray*}
$$

and see that radius oscillates with frequency $2\omega_0$. The
factor of two comes because the oscillation $x=A\cos\omega_0t$ has two
maxima for $x^2$, one at $t=0$ and one a half period later.

The following code shows first how we can solve this problem using the radial degrees of freedom only.

```python
DeltaT = 0.01
#set up arrays
tfinal = 10.0
n = ceil(tfinal/DeltaT)
# set up arrays for t, v and r
t = np.zeros(n)
v = np.zeros(n)
r = np.zeros(n)
E = np.zeros(n)
# Constants of the model
AngMom = 1.0  #  The angular momentum
m = 0.1
k = 0.1
omega02 = k/m
c1 = AngMom*AngMom/(m*m)
c2 = AngMom*AngMom/m
rmin = (AngMom*AngMom/k/m)**0.25
# Initial conditions
r0 = rmin
v0 = 0.0
r[0] = r0
v[0] = v0
E[0] = 0.5*m*v0*v0+0.5*k*r0*r0+0.5*c2/(r0*r0)
# Start integrating using the Velocity-Verlet  method
for i in range(n-1):
    # Set up acceleration
    a = -r[i]*omega02+c1/(r[i]**3)
    # update velocity, time and position using the Velocity-Verlet method
    r[i+1] = r[i] + DeltaT*v[i]+0.5*(DeltaT**2)*a
    anew = -r[i+1]*omega02+c1/(r[i+1]**3)
    v[i+1] = v[i] + 0.5*DeltaT*(a+anew)
    t[i+1] = t[i] + DeltaT
    E[i+1] = 0.5*m*v[i+1]*v[i+1]+0.5*k*r[i+1]*r[i+1]+0.5*c2/(r[i+1]*r[i+1])
    # Plot position as function of time
fig, ax = plt.subplots(2,1)
ax[0].set_xlabel('time')
ax[0].set_ylabel('radius')
ax[0].plot(t,r)
ax[1].set_xlabel('time')
ax[1].set_ylabel('Energy')
ax[1].plot(t,E)
save_fig("RadialHOVV")
plt.show()
```

## Stability of Orbits

The effective force can be extracted from the effective potential, $U_{\rm eff}$. Beginning from the equations of motion, Eq. ([2](#eq:radialeqofmotion)), for $r$,

$$
\begin{eqnarray}
m\ddot{r}&=&F+\frac{L^2}{mr^3}\\
\nonumber
&=&F_{\rm eff}\\
\nonumber
&=&-\partial_rU_{\rm eff},\\
\nonumber
F_{\rm eff}&=&-\partial_r\left[U(r)+(L^2/2mr^2)\right].
\end{eqnarray}
$$

For a circular orbit, the radius must be fixed as a function of time,
so one must be at a maximum or a minimum of the effective
potential. However, if one is at a maximum of the effective potential
the radius will be unstable. For the attractive Coulomb force the
effective potential will be dominated by the $-\alpha/r$ term for
large $r$ because the centrifugal part falls off more quickly, $\sim
1/r^2$. At low $r$ the centrifugal piece wins and the effective
potential is repulsive. Thus, the potential must have a minimum
somewhere with negative potential. The circular orbits are then stable
to perturbation.

The effective potential is sketched for two cases, a $1/r$ attractive
potential and a $1/r^3$ attractive potential. The $1/r$ case has a
stable minimum, whereas the circular orbit in the $1/r^3$ case is
unstable.

If one considers a potential that falls as $1/r^3$, the situation is
reversed and the point where $\partial_rU$ disappears will be a local
maximum rather than a local minimum. **Fig to come here with code**

The repulsive centrifugal piece dominates at large $r$ and the attractive
Coulomb piece wins out at small $r$. The circular orbit is then at a
maximum of the effective potential and the orbits are unstable. It is
the clear that for potentials that fall as $r^n$, that one must have
$n>-2$ for the orbits to be stable.

Consider a potential $U(r)=\beta r$. For a particle of mass $m$ with
angular momentum $L$, find the angular frequency of a circular
orbit. Then find the angular frequency for small radial perturbations.

For the circular orbit you search for the position $r_{\rm min}$ where the effective potential is minimized,

$$
\begin{eqnarray*}
\partial_r\left\{\beta r+\frac{L^2}{2mr^2}\right\}&=&0,\\
\beta&=&\frac{L^2}{mr_{\rm min}^3},\\
r_{\rm min}&=&\left(\frac{L^2}{\beta m}\right)^{1/3},\\
\dot{\theta}&=&\frac{L}{mr_{\rm min}^2}=\frac{\beta^{2/3}}{(mL)^{1/3}}
\end{eqnarray*}
$$

Now, we can find the angular frequency of small perturbations about the circular orbit. To do this we find the effective spring constant for the effective potential,

$$
\begin{eqnarray*}
k_{\rm eff}&=&\partial_r^2 \left.U_{\rm eff}\right|_{r_{\rm min}}\\
&=&\frac{3L^2}{mr_{\rm min}^4},\\
\omega&=&\sqrt{\frac{k_{\rm eff}}{m}}\\
&=&\frac{\beta^{2/3}}{(mL)^{1/3}}\sqrt{3}.
\end{eqnarray*}
$$

If the two frequencies, $\dot{\theta}$ and $\omega$, differ by an
integer factor, the orbit's trajectory will repeat itself each time
around. This is the case for the inverse-square force,
$\omega=\dot{\theta}$, and for the harmonic oscillator,
$\omega=2\dot{\theta}$. In this case, $\omega=\sqrt{3}\dot{\theta}$,
and the angles at which the maxima and minima occur change with each
orbit.

### Code example with gravitional force

The code example here is meant to illustrate how we can make a plot of the final orbit. We solve the equations in polar coordinates (the example here uses the minimum of the potential as initial value) and then we transform back to cartesian coordinates and plot $x$ versus $y$. We see that we get a perfect circle when we place ourselves at the minimum of the potential energy, as expected.

```python

# Simple Gravitational Force   -alpha/r

DeltaT = 0.01
#set up arrays
tfinal = 8.0
n = ceil(tfinal/DeltaT)
# set up arrays for t, v and r
t = np.zeros(n)
v = np.zeros(n)
r = np.zeros(n)
phi = np.zeros(n)
x = np.zeros(n)
y = np.zeros(n)
# Constants of the model, setting all variables to one for simplicity
alpha = 1.0
AngMom = 1.0  #  The angular momentum
m = 1.0  # scale mass to one
c1 = AngMom*AngMom/(m*m)
c2 = AngMom*AngMom/m
rmin = (AngMom*AngMom/m/alpha)
# Initial conditions, place yourself at the potential min
r0 = rmin
v0 = 0.0  # starts at rest
r[0] = r0
v[0] = v0
phi[0] = 0.0
# Start integrating using the Velocity-Verlet  method
for i in range(n-1):
    # Set up acceleration
    a = -alpha/(r[i]**2)+c1/(r[i]**3)
    # update velocity, time and position using the Velocity-Verlet method
    r[i+1] = r[i] + DeltaT*v[i]+0.5*(DeltaT**2)*a
    anew = -alpha/(r[i+1]**2)+c1/(r[i+1]**3)
    v[i+1] = v[i] + 0.5*DeltaT*(a+anew)
    t[i+1] = t[i] + DeltaT
    phi[i+1] = t[i+1]*c2/(r0**2)
# Find cartesian coordinates for easy plot
x = r*np.cos(phi)
y = r*np.sin(phi)
fig, ax = plt.subplots(3,1)
ax[0].set_xlabel('time')
ax[0].set_ylabel('radius')
ax[0].plot(t,r)
ax[1].set_xlabel('time')
ax[1].set_ylabel('Angle $\cos{\phi}$')
ax[1].plot(t,np.cos(phi))
ax[2].set_ylabel('y')
ax[2].set_xlabel('x')
ax[2].plot(x,y)

save_fig("Phasespace")
plt.show()
```

Try to change the initial value for $r$ and see what kind of orbits you get.
In order to test different energies, it can be useful to look at the plot of the effective potential discussed above.

However, for orbits different from a circle the above code would need modifications in order to allow us to display say an ellipse. For the latter, it is much easier to run our code in cartesian coordinates, as done here. In this code we test also energy conservation and see that it is conserved to numerical precision. The code here is a simple extension of the code we developed for homework 4.

```python
# Common imports
import numpy as np
import pandas as pd
from math import *
import matplotlib.pyplot as plt

DeltaT = 0.01
#set up arrays
tfinal = 10.0
n = ceil(tfinal/DeltaT)
# set up arrays
t = np.zeros(n)
v = np.zeros((n,2))
r = np.zeros((n,2))
E = np.zeros(n)
# Constants of the model
m = 1.0   # mass, you can change these
alpha = 1.0
# Initial conditions as compact 2-dimensional arrays
x0 = 0.5; y0= 0.
r0 = np.array([x0,y0])
v0 = np.array([0.0,1.0])
r[0] = r0
v[0] = v0
rabs = sqrt(sum(r[0]*r[0]))
E[0] = 0.5*m*(v[0,0]**2+v[0,1]**2)-alpha/rabs
# Start integrating using the Velocity-Verlet  method
for i in range(n-1):
    # Set up the acceleration
    rabs = sqrt(sum(r[i]*r[i]))
    a =  -alpha*r[i]/(rabs**3)
    # update velocity, time and position using the Velocity-Verlet method
    r[i+1] = r[i] + DeltaT*v[i]+0.5*(DeltaT**2)*a
    rabs = sqrt(sum(r[i+1]*r[i+1]))
    anew = -alpha*r[i+1]/(rabs**3)
    v[i+1] = v[i] + 0.5*DeltaT*(a+anew)
    E[i+1] = 0.5*m*(v[i+1,0]**2+v[i+1,1]**2)-alpha/rabs
    t[i+1] = t[i] + DeltaT
# Plot position as function of time
fig, ax = plt.subplots(3,1)
ax[0].set_ylabel('y')
ax[0].set_xlabel('x')
ax[0].plot(r[:,0],r[:,1])
ax[1].set_xlabel('time')
ax[1].set_ylabel('y position')
ax[1].plot(t,r[:,0])
ax[2].set_xlabel('time')
ax[2].set_ylabel('y position')
ax[2].plot(t,r[:,1])

fig.tight_layout()
save_fig("2DimGravity")
plt.show()
print(E)
```


This notebook is part of https://github.com/AudioSceneDescriptionFormat/splines, see also http://splines.readthedocs.io/.

# Non-Uniform Kochanek--Bartels Splines

<cite data-cite="kochanek1984tcb">Kochanek and Bartels (1984)</cite>
mainly talks about uniform splines.
Only in section 4, "Adjustments for Parameter Step Size", they briefly mention the non-uniform case.

TODO: show equations for adjusted tangents

Unfortunately, this is wrong.

TODO: show why it is wrong.

Instead, we should start from the correct tangent vector for non-uniform Catmull--Rom splines:

\begin{equation}
\boldsymbol{\dot{x}}_i =
\frac{
(t_{i+1} - t_i)^2 (\boldsymbol{x}_i - \boldsymbol{x}_{i-1}) +
(t_i - t_{i-1})^2 (\boldsymbol{x}_{i+1} - \boldsymbol{x}_i)
}{
(t_{i+1} - t_i)(t_i - t_{i-1})(t_{i+1} - t_{i-1})
}
\end{equation}

## Parameters

In general
incoming tangent $\boldsymbol{\dot{x}}_i^{(-)}$ and
outgoing tangent $\boldsymbol{\dot{x}}_i^{(+)}$ at vertex $\boldsymbol{x}_i$:

\begin{align}
a_i &= (1 - T_i) (1 + C_i) (1 + B_i)\\
b_i &= (1 - T_i) (1 - C_i) (1 - B_i)\\
c_i &= (1 - T_i) (1 - C_i) (1 + B_i)\\
d_i &= (1 - T_i) (1 + C_i) (1 - B_i)
\end{align}

\begin{align}
\boldsymbol{\dot{x}}_i^{(+)} &= \frac{
a_i (t_{i+1} - t_i)^2 (\boldsymbol{x}_i - \boldsymbol{x}_{i-1}) +
b_i (t_i - t_{i-1})^2 (\boldsymbol{x}_{i+1} - \boldsymbol{x}_i)
}{(t_{i+1} - t_i) (t_i - t_{i-1}) (t_{i+1} - t_{i-1})}\\
\boldsymbol{\dot{x}}_i^{(-)} &= \frac{
c_i (t_{i+1} - t_i)^2 (\boldsymbol{x}_i - \boldsymbol{x}_{i-1}) +
d_i (t_i - t_{i-1})^2 (\boldsymbol{x}_{i+1} - \boldsymbol{x}_i)
}{(t_{i+1} - t_i) (t_i - t_{i-1}) (t_{i+1} - t_{i-1})}
\end{align}

In the calculation below, we consider
the outgoing tangent at $\boldsymbol{x}_0$ and
the incoming tangent at $\boldsymbol{x}_1$.

\begin{align}
a_0 &= (1 - T_0) (1 + C_0) (1 + B_0)\\
b_0 &= (1 - T_0) (1 - C_0) (1 - B_0)\\
c_1 &= (1 - T_1) (1 - C_1) (1 + B_1)\\
d_1 &= (1 - T_1) (1 + C_1) (1 - B_1)
\end{align}

\begin{align}
\boldsymbol{\dot{x}}_0^{(+)} &= \frac{
a_0 (t_1 - t_0)^2 (\boldsymbol{x}_0 - \boldsymbol{x}_{-1}) +
b_0 (t_0 - t_{-1})^2 (\boldsymbol{x}_1 - \boldsymbol{x}_0)
}{(t_1 - t_0) (t_0 - t_{-1}) (t_1 - t_{-1})}\\
\boldsymbol{\dot{x}}_1^{(-)} &= \frac{
c_1 (t_2 - t_1)^2 (\boldsymbol{x}_1 - \boldsymbol{x}_0) +
d_1 (t_1 - t_0)^2 (\boldsymbol{x}_2 - \boldsymbol{x}_1)
}{(t_2 - t_1) (t_1 - t_0) (t_2 - t_0)}
\end{align}

## Calculation

```python
%matplotlib inline
from IPython.display import display
import sympy as sp
sp.init_printing()
```

```python
from utility import NamedExpression, NamedMatrix
```

```python
x_1, x0, x1, x2 = sp.symbols('xbm_-1 xbm:3')
```

```python
t, t_1, t0, t1, t2 = sp.symbols('t t_-1 t:3')
```

Same control values as Catmull-Rom ...

```python
control_values_KB = sp.Matrix([x_1, x0, x1, x2])
control_values_KB
```

... but three additional parameters per vertex.
In our calculation, the parameters belonging to $\boldsymbol{x}_0$ and $\boldsymbol{x}_1$ are relevant:

```python
T0, T1 = sp.symbols('T:2')
C0, C1 = sp.symbols('C:2')
B0, B1 = sp.symbols('B:2')
```

```python
a = NamedExpression('a', (1 - T0) * (1 + C0) * (1 + B0))
b = NamedExpression('b', (1 - T0) * (1 - C0) * (1 - B0))
c = NamedExpression('c', (1 - T1) * (1 - C1) * (1 + B1))
d = NamedExpression('d', (1 - T1) * (1 + C1) * (1 - B1))
display(a, b, c, d)
```

```python
xd0 = NamedExpression(
    'xdotbm0',
    (a.name * (t1 - t0)**2 * (x0 - x_1) + b.name * (t0 - t_1)**2 * (x1 - x0)) /
    ((t1 - t0) * (t0 - t_1) * (t1 - t_1)))
xd1 = NamedExpression(
    'xdotbm1',
    (c.name * (t2 - t1)**2 * (x1 - x0) + d.name * (t1 - t0)**2 * (x2 - x1)) /
    ((t2 - t1) * (t1 - t0) * (t2 - t0)))
display(xd0, xd1)
```

```python
display(xd0.subs([a, b]))
display(xd1.subs([c, d]))
```

Same as with Catmull-Rom, try to find a transformation from cardinal control values to Hermite control values.
This can be used to get the full characteristic matrix.

```python
control_values_H = sp.Matrix([x0, x1, xd0.name, xd1.name])
control_values_H
```

From the [notebook about non-uniform Hermite splines](hermite-non-uniform.ipynb):

```python
M_H = NamedMatrix(
    r'{M_\text{H}}',
    sp.S('Matrix([[2, -2, Delta0, Delta0], [-3, 3, -2*Delta0, -Delta0], [0, 0, Delta0, 0], [1, 0, 0, 0]])'))
M_H
```

```python
M_KBtoH = NamedMatrix(r'{M_\text{KB$\to$H}}', 4, 4)
M_KB = NamedMatrix(r'{M_\text{KB}}', M_H.name * M_KBtoH.name)
M_KB
```

```python
sp.Eq(control_values_H, M_KBtoH.name * control_values_KB)
```

If we substitute the above definitions of $\boldsymbol{\dot{x}}_0$ and $\boldsymbol{\dot{x}}_1$, we can directly read off the matrix elements:

```python
M_KBtoH.expr = sp.Matrix([[expr.coeff(cv).simplify() for cv in control_values_KB]
                          for expr in control_values_H.subs([xd0.args, xd1.args]).expand()])
M_KBtoH
```

```python
deltas = [
    (t_1, -sp.Symbol('Delta_-1')),
    (t0, 0),
    (t1, sp.Symbol('Delta0')),
    (t2, sp.Symbol('Delta0') + sp.Symbol('Delta1'))
]
```

```python
M_KBtoH.subs(deltas)
```

```python
M_KB = M_KB.subs([M_H, M_KBtoH.subs(deltas)]).doit().expand()
M_KB
```

And for completeness' sake, its inverse:

```python
M_KB.I
```

TODO: plot some example curves


# Constructing Models

In this notebook example, a step-by-step approach of building a simple model$^1$ of trafficking of high-energy phosphate bonds is demonstrated. Illustrated below is a graphical view of the full system along with the reaction rate equations and numerical values:

The example starts by creating a model of the "use", "distr", and "form" reactions. Then the model is expanded to include the remaining metabolites, reactions, and any additional information that should be defined in the model.

## Creating a Model

```python
import numpy as np
import pandas as pd

from mass import (
    MassConfiguration, MassMetabolite, MassModel, MassReaction)
from mass.util.matrix import left_nullspace, nullspace

mass_config = MassConfiguration()
```

The first step to creating the model is to define the `MassModel` object. A `MassModel` only requires an identifier to be initialized. For best practice, it is recommended to utilize SBML compliant identifiers for all objects.

```python
model = MassModel("Phosphate_Trafficking")
```

    Set parameter Username

The model is initially empty.

```python
print("Number of metabolites: {0}".format(len(model.metabolites)))
print("Number of initial conditions: {0}".format(len(model.initial_conditions)))
print("Number of reactions: {0}".format(len(model.reactions)))
```

    Number of metabolites: 0
    Number of initial conditions: 0
    Number of reactions: 0

The next step is to create `MassMetabolite` and `MassReaction` objects to represent the metabolites and reactions that should exist in the model.

### Defining metabolites

To create a `MassMetabolite`, a unique identifier is required. The `formula` and `charge` attributes are set to ensure mass and charge balancing of reactions in which the metabolite is a participant. The `compartment` attribute indicates where the metabolite is located. In this model, all metabolites exist in a single compartment, abbreviated as "c".

```python
atp_c = MassMetabolite(
    "atp_c",
    name="ATP",
    formula="C10H12N5O13P3",
    charge=-4,
    compartment="c")

adp_c = MassMetabolite(
    "adp_c",
    name="ADP",
    formula="C10H12N5O10P2",
    charge=-3,
    compartment="c")

amp_c = MassMetabolite(
    "amp_c",
    name="AMP",
    formula="C10H12N5O7P",
    charge=-2,
    compartment="c")
```

The metabolite concentrations can be defined as the initial conditions for the metabolites using the `initial_condition` attribute. As previously stated, the concentrations are $\text{[ATP]}=1.6$, $\text{[ADP]}=0.4$, and $\text{[AMP]}=0.1$.

```python
atp_c.initial_condition = 1.6
adp_c.ic = 0.4  # Alias for initial_condition
amp_c.ic = 0.1

for metabolite in [atp_c, adp_c, amp_c]:
    print("{0}: {1}".format(metabolite.id, metabolite.initial_condition))
```

    atp_c: 1.6
    adp_c: 0.4
    amp_c: 0.1

The metabolites are currently not a part of any reaction. Consequently, the `ordinary_differential_equation` attribute is `None`.

```python
print(atp_c.ordinary_differential_equation)
print(adp_c.ode)  # Alias for ordinary_differential_equation
print(amp_c.ode)
```

    None
    None
    None

The next step is to create the reactions in which the metabolites participate.

### Defining reactions

Just like `MassMetabolite` objects, a unique identifier is also required to create a `MassReaction`. The `reversible` attribute determines whether the reaction can proceed in both the forward and reverse directions, or only in the forward direction.

```python
distr = MassReaction("distr", name="Distribution", reversible=True)
use = MassReaction("use", name="ATP Utilization", reversible=False)
form = MassReaction("form", name="ATP Formation", reversible=False)
```

Metabolites are added to reactions using a dictionary of metabolite objects and their stoichiometric coefficients. A group of metabolites can be added either all at once or one at a time. A negative coefficient indicates the metabolite is a reactant, while a positive coefficient indicates the metabolite is a product.

```python
distr.add_metabolites({
    adp_c: -2,
    atp_c: 1,
    amp_c: 1})

use.add_metabolites({
    atp_c: -1,
    adp_c: 1})

form.add_metabolites({
    adp_c: -1,
    atp_c: 1})

for reaction in [distr, use, form]:
    print(reaction)
```

    distr: 2 adp_c <=> amp_c + atp_c
    use: atp_c --> adp_c
    form: adp_c --> atp_c

Once the reactions are created, their parameters can be defined. As stated earlier, the distribution reaction is considerably faster when compared to other reactions in the model. The forward rate constant $k^{\rightarrow}$, represented as `kf`, can be set as $k^{\rightarrow}_{distr}=1000\ \text{min}^{-1}$. The equilibrium constant $K_{eq}$, represented as `Keq`, is approximately $K_{distr}=1$.

```python
distr.forward_rate_constant = 1000
distr.equilibrium_constant = 1
distr.parameters  # Return defined mass action kinetic parameters
```

    {'kf_distr': 1000, 'Keq_distr': 1}

As shown earlier, the forward rate constants are set as $k^{\rightarrow}_{use}=6.25\ \text{min}^{-1}$ and $k^{\rightarrow}_{form}=25\ \text{min}^{-1}$.
The `kf_str` attribute can be used to get the identifier of the forward rate constant as a string.

```python
use.forward_rate_constant = 6.25
form.kf = 25  # Alias for forward_rate_constant

print("{0}: {1}".format(use.kf_str, use.kf))
print("{0}: {1}".format(form.kf_str, form.kf))
```

    kf_use: 6.25
    kf_form: 25

Reactions can be added to the model using the `add_reactions()` method. Adding the reactions to the model also adds the associated metabolites and genes.

```python
model.add_reactions([distr, use, form])

print("Number of metabolites: {0}".format(len(model.metabolites)))
print("Number of initial conditions: {0}".format(len(model.initial_conditions)))
print("Number of reactions: {0}".format(len(model.reactions)))
```

    Number of metabolites: 3
    Number of initial conditions: 3
    Number of reactions: 3

The stoichiometric matrix of the model is automatically constructed with the addition of the reactions and metabolites to the model. It can be accessed through the `stoichiometric_matrix` property (alias `S`).

```python
print(model.S)
```

    [[-2.  1. -1.]
     [ 1. -1.  1.]
     [ 1.  0.  0.]]

The stoichiometric matrix attribute can be updated and stored in various formats using the `update_S()` method. For example, the stoichiometric matrix can be converted and stored as a `pandas.DataFrame`.

```python
model.update_S(array_type="DataFrame", dtype=np.int_, update_model=True)
model.S
```

      distr
      use
      form

      adp_c
      -2
      1
      -1

      atp_c
      1
      -1
      1

      amp_c
      1
      0
      0

Associating the metabolites with reactions allows for the mass action reaction rate expressions to be generated based on the stoichiometry.

```python
print(distr.rate)
```

    kf_distr*(adp_c(t)**2 - amp_c(t)*atp_c(t)/Keq_distr)

Generation of the reaction rates also allows for the metabolite ODEs to be generated.

```python
print(atp_c.ode)
```

    kf_distr*(adp_c(t)**2 - amp_c(t)*atp_c(t)/Keq_distr) + kf_form*adp_c(t) - kf_use*atp_c(t)

The `nullspace()` method can be used to obtain the null space of the stoichiometric matrix. The nullspace reflects the pathways through the system.

```python
ns = nullspace(model.S)  # Get the null space
# Divide by the minimum and round to nearest integer
ns = np.rint(ns / np.min(ns[np.nonzero(ns)]))
pd.DataFrame(
    ns, index=model.reactions.list_attr("id"),  # Rows represent reactions
    columns=["Pathway 1"], dtype=np.int_)
```

      Pathway 1

      distr
      0

      use
      1

      form
      1

In a similar fashion the left nullspace can be obtained using the `left_nullspace` function. The left nullspace represents the conserved moieties in the model.

```python
lns = left_nullspace(model.S)
# Divide by the minimum and round to nearest integer
lns = np.rint(lns / np.min(lns[np.nonzero(lns)]))
pd.DataFrame(
    lns, index=["Total AxP"],
    columns=model.metabolites.list_attr("id"),  # Columns represent metabolites
    dtype=np.int_)
```

      adp_c
      atp_c
      amp_c

      Total AxP
      1
      1
      1

## Expanding an Existing Model

Now, the existing model is expanded to include a buffer reaction, where a phosphagen is utilized to store a high-energy phosphate in order to buffer the ATP/ADP ratio as needed. Because the buffer molecule represents a generic phosphagen, there is no chemical formula for the molecule. Therefore, the buffer molecule can be represented as a moiety in the `formula` attribute using square brackets.

```python
b = MassMetabolite(
    "B",
    name="Phosphagen buffer (Free)",
    formula="[B]",
    charge=0,
    compartment="c")

bp = MassMetabolite(
    "BP",
    name="Phosphagen buffer (Loaded)",
    formula="[B]-PO3",
    charge=-1,
    compartment="c")

buffer = MassReaction("buffer", name="ATP Buffering")
```

When adding metabolites to the reaction, the `get_by_id()` method is used to add already existing metabolites in the model to the reaction.

```python
buffer.add_metabolites({
    b: -1,
    model.metabolites.get_by_id("atp_c"): -1,
    model.metabolites.get_by_id("adp_c"): 1,
    bp: 1})

# Add reaction to model
model.add_reactions([buffer])
```

For this reaction, $k^{\rightarrow}_{buffer}=1000\ \text{min}^{-1}$ and $K_{buffer}=1$. Because the reaction has already been added to the model, the `MassModel.update_parameters()` method can be used to update the reaction parameters using a dictionary:

```python
model.update_parameters({
    buffer.kf_str: 1000,
    buffer.Keq_str: 1})

buffer.parameters
```

    {'kf_buffer': 1000, 'Keq_buffer': 1}

By adding the reaction to the model, the left nullspace expanded to include a conservation pool for the total buffer in the system.

```python
lns = left_nullspace(model.S)
for i, row in enumerate(lns):
    # Divide by the minimum and round to nearest integer
    lns[i] = np.rint(row / np.min(row[np.nonzero(row)]))
pd.DataFrame(lns, index=["Total AxP", "Total Buffer"],
             columns=model.metabolites.list_attr("id"),
             dtype=np.int_)
```

      adp_c
      atp_c
      amp_c
      B
      BP

      Total AxP
      1
      1
      1
      0
      0

      Total Buffer
      0
      0
      0
      1
      1

### Performing symbolic calculations

Although the concentrations for the free and loaded buffer molecules are currently unknown, the total amount of buffer is known and set as $B_{total} = 10$. Because the buffer reaction is assumed to be at equilibrium, it becomes possible to solve for the concentrations of the free and loaded buffer molecules.

Below, the symbolic capabilities of **SymPy** are used to solve for the steady state concentrations of the buffer molecules.

```python
from sympy import Eq, Symbol, pprint, solve

from mass.util import strip_time
```

The first step is to define the equation for the total buffer pool symbolically:

```python
buffer_total_equation = Eq(Symbol("B") + Symbol("BP"), 10)
pprint(buffer_total_equation)
```

    B + BP = 10

The equation for the reaction rate at equilibrium is also defined. The `strip_time()` function is used to strip time dependency from the equation.

```python
buffer_rate_equation = Eq(0, strip_time(buffer.rate))
# Substitute defined concentration values into equation
buffer_rate_equation = buffer_rate_equation.subs({
    "atp_c":  atp_c.initial_condition,
    "adp_c":  adp_c.initial_condition,
    "kf_buffer": buffer.kf,
    "Keq_buffer": buffer.Keq})
pprint(buffer_rate_equation)
```

    0 = 1600.0⋅B - 400.0⋅BP

These two equations can be solved to get the buffer concentrations:

```python
buffer_sol = solve([buffer_total_equation, buffer_rate_equation],
                   [Symbol("B"), Symbol("BP")])
buffer_sol
```

    {B: 2.00000000000000, BP: 8.00000000000000}

Because the metabolites already exist in the model, their initial conditions can be updated to the calculated concentrations using the `MassModel.update_initial_conditions()` method.

```python
# Replace the symbols in the dict
for met_symbol, concentration in buffer_sol.copy().items():
    metabolite = model.metabolites.get_by_id(str(met_symbol))
    # Make value as a float
    buffer_sol[metabolite] = float(buffer_sol.pop(met_symbol))

model.update_initial_conditions(buffer_sol)
model.initial_conditions
```

    {<MassMetabolite adp_c at 0x7fc90922c130>: 0.4,
     <MassMetabolite atp_c at 0x7fc90922c190>: 1.6,
     <MassMetabolite amp_c at 0x7fc90922c1c0>: 0.1,
     <MassMetabolite B at 0x7fc8e9469cd0>: 2.0,
     <MassMetabolite BP at 0x7fc8e9469c70>: 8.0}

### Adding boundary reactions

After adding the buffer reactions, the next step is to define the AMP source and demand reactions. The `add_boundary()` method is employed to create and add a boundary reaction to a model.

```python
amp_drain = model.add_boundary(
    model.metabolites.amp_c,
    boundary_type="demand",
    reaction_id="amp_drain")

amp_in = model.add_boundary(
    model.metabolites.amp_c,
    boundary_type="demand",
    reaction_id="amp_in")

print(amp_drain)
print(amp_in)
```

    amp_drain: amp_c -->
    amp_in: amp_c -->

When a boundary reaction is created, a 'boundary metabolite' is also created as a proxy metabolite. The proxy metabolite is the external metabolite concentration (i.e., boundary condition) without instantiating a new `MassMetabolite` object to represent the external metabolite.

```python
amp_in.boundary_metabolite
```

    'amp_b'

The value of the 'boundary metabolite' can be set using the `MassModel.add_boundary_conditions()` method. Boundary conditions are accessed through the `MassModel.boundary_conditions` attribute.

```python
model.add_boundary_conditions({amp_in.boundary_metabolite: 1})
model.boundary_conditions
```

    {'amp_b': 1.0}

The automatic generation of the boundary reaction can be useful. However, sometimes the reaction stoichiometry needs to be switched in order to be intuitive. In this case, the stoichiometry of the AMP source reaction should be reversed to show that AMP enters the system, which is accomplished by using the `MassReaction.reverse_stoichiometry()` method.

```python
amp_in.reverse_stoichiometry(inplace=True)
print(amp_in)
```

    amp_in:  --> amp_c

Note that the addition of these two reactions adds an another pathway to the null space:

```python
ns = nullspace(model.S).T
for i, row in enumerate(ns):
    # Divide by the minimum to get all integers
    ns[i] = np.rint(row / np.min(row[np.nonzero(row)]))
ns = ns.T
pd.DataFrame(
    ns, index=model.reactions.list_attr("id"),  # Rows represent reactions
    columns=["Path 1", "Path 2"], dtype=np.int_)
```

      Path 1
      Path 2

      distr
      0
      0

      use
      1
      0

      form
      1
      0

      buffer
      0
      0

      amp_drain
      0
      1

      amp_in
      0
      1

### Defining custom rates

In this model, the rate for the AMP source reaction should remain at a fixed input value. However, the current rate expression for the AMP source reaction is dependent on an external AMP metabolite that exists as a boundary condition:

```python
print(amp_in.rate)
```

    amp_b*kf_amp_in

Therefore, the rate can be set as a fixed input by using a custom rate expression. Custom rate expressions can be set for reactions in a model using the `MassModel.add_custom_rate()` method as follows: by passing the reaction object, a string representation of the custom rate expression, and a dictionary containing any custom parameter associated with the rate.

```python
model.add_custom_rate(amp_in, custom_rate="b1",
                      custom_parameters={"b1": 0.03})
print(model.rates[amp_in])
```

    b1

## Ensuring Model Completeness
### Inspecting rates and ODEs

According to the [network schema](#Constructing-Models) at the start of the notebook, the network has been fully reconstructed. The reaction rates and metabolite ODEs can be inspected to ensure that the model was built without any issues.

The `MassModel.rates` property is used to return a dictionary containing reactions and symbolic expressions of their rates. The model always prioritizes custom rate expressions over automatically generated mass action rates.

```python
for reaction, rate in model.rates.items():
    print("{0}: {1}".format(reaction.id, rate))
```

    distr: kf_distr*(adp_c(t)**2 - amp_c(t)*atp_c(t)/Keq_distr)
    use: kf_use*atp_c(t)
    form: kf_form*adp_c(t)
    buffer: kf_buffer*(B(t)*atp_c(t) - BP(t)*adp_c(t)/Keq_buffer)
    amp_drain: kf_amp_drain*amp_c(t)
    amp_in: b1

Similarly, the model can access the ODEs for metabolites using the `ordinary_differential_equations` property (alias `odes`) to return a dictionary of metabolites and symbolic expressions of their ODEs.

```python
for metabolite, ode in model.odes.items():
    print("{0}: {1}".format(metabolite.id, ode))
```

    adp_c: kf_buffer*(B(t)*atp_c(t) - BP(t)*adp_c(t)/Keq_buffer) - 2*kf_distr*(adp_c(t)**2 - amp_c(t)*atp_c(t)/Keq_distr) - kf_form*adp_c(t) + kf_use*atp_c(t)
    atp_c: -kf_buffer*(B(t)*atp_c(t) - BP(t)*adp_c(t)/Keq_buffer) + kf_distr*(adp_c(t)**2 - amp_c(t)*atp_c(t)/Keq_distr) + kf_form*adp_c(t) - kf_use*atp_c(t)
    amp_c: b1 - kf_amp_drain*amp_c(t) + kf_distr*(adp_c(t)**2 - amp_c(t)*atp_c(t)/Keq_distr)
    B: -kf_buffer*(B(t)*atp_c(t) - BP(t)*adp_c(t)/Keq_buffer)
    BP: kf_buffer*(B(t)*atp_c(t) - BP(t)*adp_c(t)/Keq_buffer)

### Compartments

Compartments, defined in metabolites, are recognized by the model and can be viewed in the `compartments` attribute.

```python
model.compartments
```

    {'c': ''}

For this model, "c" is an abbreviation for "compartment". The `compartments` attribute can be updated to reflect this mapping using a `dict`:

```python
model.compartments = {"c": "compartment"}
model.compartments
```

    {'c': 'compartment'}

### Units
`Unit` and `UnitDefinition` objects are implemented as per the [SBML Unit](http://sbml.org/Software/libSBML/5.18.0/docs/python-api/classlibsbml_1_1_unit.html) and [SBML UnitDefinition](http://sbml.org/Software/libSBML/5.18.0/docs/python-api/classlibsbml_1_1_unit_definition.html) specifications.
It can be useful for comparative reasons to create `Unit` and `UnitDefinition` objects for the model (e.g., amount, volume, time) to provide additional context. However, the model does not maintain unit consistency automatically. It is the responsibility of the users to ensure consistency among units and associated numerical values in a model.

```python
from mass import Unit, UnitDefinition
from mass.core.units import print_defined_unit_values
```

SBML defines units using a compositional approach. The `Unit` objects represent references to base units. A `Unit` has four attributes: `kind`, `exponent`, `scale`, and `multiplier`. The `kind` attribute indicates the base unit. Valid base units are viewed using the `print_defined_unit_values()` function.

```python
print_defined_unit_values("BaseUnitKinds")
```

    ╒═════════════════════════════╕
    │ SBML Base Unit Kinds        │
    ╞═════════════════════════════╡
    │ Base Unit        SBML Value │
    │ -------------  ------------ │
    │ ampere                    0 │
    │ avogadro                  1 │
    │ becquerel                 2 │
    │ candela                   3 │
    │ coulomb                   5 │
    │ dimensionless             6 │
    │ farad                     7 │
    │ gram                      8 │
    │ gray                      9 │
    │ henry                    10 │
    │ hertz                    11 │
    │ item                     12 │
    │ joule                    13 │
    │ katal                    14 │
    │ kelvin                   15 │
    │ kilogram                 16 │
    │ liter                    17 │
    │ litre                    18 │
    │ lumen                    19 │
    │ lux                      20 │
    │ meter                    21 │
    │ metre                    22 │
    │ mole                     23 │
    │ newton                   24 │
    │ ohm                      25 │
    │ pascal                   26 │
    │ radian                   27 │
    │ second                   28 │
    │ siemens                  29 │
    │ sievert                  30 │
    │ steradian                31 │
    │ tesla                    32 │
    │ volt                     33 │
    │ watt                     34 │
    │ weber                    35 │
    │ invalid                  36 │
    ╘═════════════════════════════╛

The `exponent`, `scale` and `multiplier` attributes indicate how the base unit should be transformed. For this model, the unit for concentration, "Millimolar", which is represented as millimole per liter and composed of the following base units:

```python
millimole = Unit(kind="mole", exponent=1, scale=-3, multiplier=1)
per_liter = Unit(kind="liter", exponent=-1, scale=1, multiplier=1)
```

Combinations of `Unit` objects are contained inside a `UnitDefintion` object. `UnitDefinition` objects have three attributes: an `id`, an optional `name` to represent the combination, and a `list_of_units` attribute that contain references to the `Unit` objects. The concentration unit “Millimolar” is abbreviated as “mM” and defined as follows:

```python
concentration_unit = UnitDefinition(id="mM", name="Millimolar",
                                    list_of_units=[millimole, per_liter])
print("{0}:\n{1!r}\n{2!r}".format(
    concentration_unit.name, *concentration_unit.list_of_units))
```

    Millimolar:
    <Unit at 0x7fc9194fcd00 kind: liter; exponent: -1; scale: 1; multiplier: 1>
    <Unit at 0x7fc9194fca30 kind: mole; exponent: 1; scale: -3; multiplier: 1>

`UnitDefinition` objects also have the `UnitDefinition.create_unit()` method to directly create `Unit` objects within the `UnitDefintion`.

```python
time_unit = UnitDefinition(id="min", name="Minute")
time_unit.create_unit(kind="second", exponent=1, scale=1, multiplier=60)
print(time_unit)
print(time_unit.list_of_units)
```

    min
    [<Unit at 0x7fc9194ec9a0 kind: second; exponent: 1; scale: 1; multiplier: 60>]

Once created, `UnitDefintion` objects are added to the model:

```python
model.add_units([concentration_unit, time_unit])
model.units
```

    [<UnitDefinition Millimolar "mM" at 0x7fc919500f70>,
     <UnitDefinition Minute "min" at 0x7fc8e944feb0>]

### Checking model completeness

Once constructed, the model should be checked for completeness.

```python
from mass import qcqa_model
```

The `qcqa_model()` function can be used to print a report about the model's completeness based on the set kwargs. The `qcqa_model()` function is used to ensure that all numerical values necessary for simulating the model are defined by setting the `parameters` and `concentrations` kwargs as `True`.

```python
qcqa_model(model, parameters=True, concentrations=True)
```

    ╒══════════════════════════════════════════════╕
    │ MODEL ID: Phosphate_Trafficking              │
    │ SIMULATABLE: False                           │
    │ PARAMETERS NUMERICALY CONSISTENT: True       │
    ╞══════════════════════════════════════════════╡
    │ ============================================ │
    │             MISSING PARAMETERS               │
    │ ============================================ │
    │ Reaction Parameters                          │
    │ ---------------------                        │
    │ amp_drain: kf                                │
    │ ============================================ │
    ╘══════════════════════════════════════════════╛

As shown in the report above, the forward rate constant for the AMP drain reaction was never defined. Therefore, the forward rate constant is defined, and the model is checked again.

```python
amp_drain.kf = 0.03

qcqa_model(model, parameters=True, concentrations=True)
```

    ╒══════════════════════════════════════════╕
    │ MODEL ID: Phosphate_Trafficking          │
    │ SIMULATABLE: True                        │
    │ PARAMETERS NUMERICALY CONSISTENT: True   │
    ╞══════════════════════════════════════════╡
    ╘══════════════════════════════════════════╛

Now, the report shows that the model is not missing any values necessary for simulation. See [Checking Model Quality](./quality_assurance.ipynb) for more information on quality assurance functions and the `qcqa` submodule.

## Additional Examples
For additional examples on constructing models, see the following:

* [Constructing Glycolysis](../gallery/workflows/constructing_glycolysis.ipynb)

$^1$ Trafficking model is created from Chapter 8 of Systems Biology: Simulation of Dynamic Network States


# Start-to-Finish Example: Unit Testing `GiRaFFE_NRPy`: $A_k$ to $B^i$

## Author: Patrick Nelson
### Formatting improvements courtesy Brandon Clark

## This module Validates the A-to-B routine for `GiRaFFE`.

**Notebook Status:** Validated

**Validation Notes:** This module will validate the routines in [Tutorial-GiRaFFE_NRPy-A2B](Tutorial-GiRaFFE_NRPy-A2B.ipynb).

### NRPy+ Source Code for this module:
* [GiRaFFE_NRPy/GiRaFFE_NRPy_A2B.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_A2B.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-A2B.ipynb) Generates the driver to compute the magnetic field from the vector potential in arbitrary spactimes.

## Introduction:

This notebook validates our A-to-B solver for use in `GiRaFFE_NRPy`. Because the original `GiRaFFE` used staggered grids and we do not, we can not trivially do a direct comparison to the old code. Instead, we will compare the numerical results with the expected analytic results.

It is, in general, good coding practice to unit test functions individually to verify that they produce the expected and intended output. Here, we expect our functions to produce the correct cross product in an arbitrary spacetime. To that end, we will choose functions that are easy to differentiate, but lack the symmetries that would trivialize the finite-difference algorithm. Higher-order polynomials are one such type of function.

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#setup): Set up core functions and parameters for unit testing the A2B algorithm
    1. [Step 1.a](#polynomial) Polynomial vector potential
    1. [Step 1.b](#gaussian) Gaussian vector potential
    1. [Step 1.c](#free_parameters) Set free parameters in the code
    1. [Step 1.d](#magnetic) The magnetic field $B^i$
    1. [Step 1.e](#vector_potential) The vector potential $A_k$
1. [Step 2](#mainc): `A2B_unit_test.c`: The Main C Code
    1. [Step 2.a](#compile_run): Compile and run the code
1. [Step 3](#convergence): Code validation: Verify that relative error in numerical solution converges to zero at the expected order
1. [Step 4](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Set up core functions and parameters for unit testing the A2B algorithm \[Back to [top](#toc)\]

$$\label{setup}$$

We'll start by appending the relevant paths to `sys.path` so that we can access sympy modules in other places. Then, we'll import NRPy+ core functionality and set up a directory in which to carry out our test. We must also set the desired finite differencing order.

```python
import shutil, os, sys           # Standard Python modules for multiplatform OS-level functions
# First, we'll add the parent directory to the list of directories Python will check for modules.
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)
nrpy_dir_path = os.path.join("..","..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

from outputC import *            # NRPy+: Core C code output module
import finite_difference as fin  # NRPy+: Finite difference C code generation module
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import grid as gri               # NRPy+: Functions having to do with numerical grids
import loop as lp                # NRPy+: Generate C code loops
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import reference_metric as rfm   # NRPy+: Reference metric support
import cmdline_helper as cmd     # NRPy+: Multi-platform Python command-line interface

out_dir = "Validation/"
cmd.mkdir(out_dir)

thismodule = "Start_to_Finish-GiRaFFE_NRPy-A2B"

# Set the finite-differencing order to 2
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER", 2)

Use_Gaussian_Data = True

```

## Step 1.a: Polynomial vector potential

$$\label{polynomial}$$

We will start with the simplest case - testing the second-order solver. In second-order finite-differencing, we use a three-point stencil that can exactly differentiate polynomials up to quadratic. So, we will use cubic functions three variables. For instance,

\begin{align}
A_x &= ax^3 + by^3 + cz^3 + dy^2 + ez^2 + f \\
A_y &= gx^3 + hy^3 + lz^3 + mx^2 + nz^2 + p \\
A_z &= px^3 + qy^3 + rz^3 + sx^2 + ty^2 + u. \\
\end{align}

It will be much simpler to let NRPy+ handle most of this work. So, we will import the core functionality of NRPy+, build the expressions, and then output them using `outputC()`.

```python
if not Use_Gaussian_Data:
    a,b,c,d,e,f,g,h,l,m,n,o,p,q,r,s,t,u = par.Cparameters("REAL",thismodule,["a","b","c","d","e","f","g","h","l","m","n","o","p","q","r","s","t","u"],1e300)
    is_gaussian = par.Cparameters("int",thismodule,"is_gaussian",0)
    gammadet = gri.register_gridfunctions("AUXEVOL","gammadet")

    par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
    rfm.reference_metric()
    x = rfm.xxCart[0]
    y = rfm.xxCart[1]
    z = rfm.xxCart[2]

    AD = ixp.register_gridfunctions_for_single_rank1("EVOL","AD")
    AD[0] = a*x**3 + b*y**3 + c*z**3 + d*y**2 + e*z**2 + f
    AD[1] = g*x**3 + h*y**3 + l*z**3 + m*x**2 + n*z**2 + o
    AD[2] = p*x**3 + q*y**3 + r*z**3 + s*x**2 + t*y**2 + u

```

## Step 1.b: Gaussian vector potential

$$\label{gaussian}$$

Alternatively, we might want to use different functions for the vector potential. Here, we'll give some 3D Gaussians:
\begin{align}
A_x &= a e^{-((x-b)^2+(y-c)^2+(z-d)^2)} \\
A_x &= f e^{-((x-g)^2+(y-h)^2+(z-l)^2)} \\
A_x &= m e^{-((x-n)^2+(y-o)^2+(z-p)^2)}, \\
\end{align}
where $e$ is the natural number.

```python
if Use_Gaussian_Data:
    a,b,c,d,e,f,g,h,l,m,n,o,p = par.Cparameters("REAL",thismodule,["a","b","c","d","e","f","g","h","l","m","n","o","p"],1e300)
    is_gaussian = par.Cparameters("int",thismodule,"is_gaussian",1)
    gammadet = gri.register_gridfunctions("AUXEVOL","gammadet")

    par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
    rfm.reference_metric()
    x = rfm.xxCart[0]
    y = rfm.xxCart[1]
    z = rfm.xxCart[2]

    AD = ixp.register_gridfunctions_for_single_rank1("EVOL","AD")
    AD[0] = a * sp.exp(-((x-b)**2 + (y-c)**2 + (z-d)**2)/(0.05**2))
    AD[1] = f * sp.exp(-((x-g)**2 + (y-h)**2 + (z-l)**2)/(0.05**2))
    AD[2] = m * sp.exp(-((x-n)**2 + (y-o)**2 + (z-p)**2)/(0.05**2))
```

## Step 1.c: Set free parameters in the code

$$\label{free_parameters}$$

We also need to create the files that interact with NRPy's C parameter interface. (This is for the newer version of the A2B routine, as we replace the old with it in this unit test.)

```python
# Step 3.d.i: Generate declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
par.generate_Cparameters_Ccodes(os.path.join(out_dir))

# Step 3.d.ii: Set free_parameters.h
with open(os.path.join(out_dir,"free_parameters.h"),"w") as file:
    file.write("""
// Override parameter defaults with values based on command line arguments and NGHOSTS.
// We'll use this grid. It has one point and one ghost zone.
params.Nxx0 = atoi(argv[1]);
params.Nxx1 = atoi(argv[2]);
params.Nxx2 = atoi(argv[3]);
params.Nxx_plus_2NGHOSTS0 = params.Nxx0 + 2*NGHOSTS;
params.Nxx_plus_2NGHOSTS1 = params.Nxx1 + 2*NGHOSTS;
params.Nxx_plus_2NGHOSTS2 = params.Nxx2 + 2*NGHOSTS;
// Step 0d: Set up space and time coordinates
// Step 0d.i: Declare \Delta x^i=dxx{0,1,2} and invdxx{0,1,2}, as well as xxmin[3] and xxmax[3]:
const REAL xxmin[3] = {-0.01,-0.01,-0.01};
const REAL xxmax[3] = { 0.01, 0.01, 0.01};

params.dxx0 = (xxmax[0] - xxmin[0]) / ((REAL)params.Nxx_plus_2NGHOSTS0-1.0);
params.dxx1 = (xxmax[1] - xxmin[1]) / ((REAL)params.Nxx_plus_2NGHOSTS1-1.0);
params.dxx2 = (xxmax[2] - xxmin[2]) / ((REAL)params.Nxx_plus_2NGHOSTS2-1.0);
printf("dxx0,dxx1,dxx2 = %.5e,%.5e,%.5e\\n",params.dxx0,params.dxx1,params.dxx2);
params.invdx0 = 1.0 / params.dxx0;
params.invdx1 = 1.0 / params.dxx1;
params.invdx2 = 1.0 / params.dxx2;
\n""")

# Generates declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
par.generate_Cparameters_Ccodes(os.path.join(out_dir))
```

## Step 1.d: The magnetic field $B^i$

$$\label{magnetic}$$

Next, we'll let NRPy+ compute derivatives analytically according to $$B^i = \frac{[ijk]}{\sqrt{\gamma}} \partial_j A_k.$$ Then we can carry out two separate tests to verify the numerical derivatives. First, we will verify that when we let the cubic terms be zero, the two calculations of $B^i$ agree to roundoff error. Second, we will verify that when we set the cubic terms, our error is dominated by trunction error that converges to zero at the expected rate.

```python
import WeylScal4NRPy.WeylScalars_Cartesian as weyl
LeviCivitaDDD = weyl.define_LeviCivitaSymbol_rank3()
LeviCivitaUUU = ixp.zerorank3()
for i in range(3):
    for j in range(3):
        for k in range(3):
            LeviCivitaUUU[i][j][k] = LeviCivitaDDD[i][j][k] / sp.sqrt(gammadet)

B_analyticU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","B_analyticU")
for i in range(3):
    B_analyticU[i] = 0
    for j in range(3):
        for k in range(3):
            B_analyticU[i] += LeviCivitaUUU[i][j][k] * sp.diff(AD[k],rfm.xxCart[j])

```

We also should write a function that will use the analytic formulae for $B^i$. Then, we'll need to call the function from the module `GiRaFFE_HO_A2B` to generate the different header files. Also, we will declare the parameters for the vector potential functions.

```python
B_analyticU_to_print = [\
                           lhrh(lhs=gri.gfaccess("out_gfs","B_analyticU0"),rhs=B_analyticU[0]),\
                           lhrh(lhs=gri.gfaccess("out_gfs","B_analyticU1"),rhs=B_analyticU[1]),\
                           lhrh(lhs=gri.gfaccess("out_gfs","B_analyticU2"),rhs=B_analyticU[2]),\
                          ]

desc = "Calculate the exact magnetic field"
name = "calculate_exact_BU"
outCfunction(
    outfile  = os.path.join(out_dir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *restrict params,REAL *restrict xx[3],REAL *restrict auxevol_gfs",
    body     = fin.FD_outputC("returnstring",B_analyticU_to_print,params="outCverbose=False").replace("IDX4","IDX4S"),
    loopopts="AllPoints,Read_xxs")

gri.glb_gridfcs_list = []
# cmd.mkdir(os.path.join(out_dir))
import GiRaFFE_NRPy.GiRaFFE_NRPy_A2B as A2B
# We'll generate these into the A2B subdirectory since that's where the functions
# we're testing expect them to be.
A2B.GiRaFFE_NRPy_A2B(os.path.join(out_dir))

```

    Output C function calculate_exact_BU() to file Validation/calculate_exact_BU.h

## Step 1.e: The vector potential $A_k$

$$\label{vector_potential}$$

We'll now write a function to set the vector potential $A_k$. This simply uses NRPy+ to generte most of the code from the expressions we wrote at the beginning.

```python
AD_to_print = [\
               lhrh(lhs=gri.gfaccess("out_gfs","AD0"),rhs=AD[0]),\
               lhrh(lhs=gri.gfaccess("out_gfs","AD1"),rhs=AD[1]),\
               lhrh(lhs=gri.gfaccess("out_gfs","AD2"),rhs=AD[2]),\
              ]

desc = "Calculate the vector potential"
name = "calculate_AD"
outCfunction(
    outfile  = os.path.join(out_dir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *restrict params,REAL *restrict xx[3],REAL *restrict out_gfs",
    body     = fin.FD_outputC("returnstring",AD_to_print,params="outCverbose=False").replace("IDX4","IDX4S"),
    loopopts="AllPoints,Read_xxs")

```

    Output C function calculate_AD() to file Validation/calculate_AD.h

# Step 2: `A2B_unit_test.c`: The Main C Code \[Back to [top](#toc)\]
$$\label{mainc}$$

Now that we have our vector potential and analytic magnetic field to compare against, we will start writing our unit test. We'll also import common C functionality, define `REAL`, the number of ghost zones, and the faces, and set the standard macros for NRPy+ style memory access.

```python
%%writefile $out_dir/A2B_unit_test.c
// These are common packages that we are likely to need.
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "string.h" // Needed for strncmp, etc.
#include "stdint.h" // Needed for Windows GCC 6.x compatibility
#include <time.h>   // Needed to set a random seed.

#define REAL double
#include "declare_Cparameters_struct.h"

const int MAXFACE = -1;
const int NUL     = +0;
const int MINFACE = +1;
const int NGHOSTS = 3;
const int NGHOSTS_A2B = 1;

REAL a,b,c,d,e,f,g,h,l,m,n,o,p,q,r,s,t,u;

// Standard NRPy+ memory access:
#define IDX4S(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (g) ) ) )

```

    Overwriting Validation//A2B_unit_test.c

We'll now define the gridfunction names.

```python
%%writefile -a $out_dir/A2B_unit_test.c
// Let's also #define the NRPy+ gridfunctions
#define AD0GF 0
#define AD1GF 1
#define AD2GF 2
#define NUM_EVOL_GFS 3

#define GAMMADETGF 0
#define B_ANALYTICU0GF 1
#define B_ANALYTICU1GF 2
#define B_ANALYTICU2GF 3
#define BU0GF 4
#define BU1GF 5
#define BU2GF 6
#define NUM_AUXEVOL_GFS 7

```

    Appending to Validation//A2B_unit_test.c

Now, we'll handle the different A2B codes. There are several things to do here. First, we'll add `#include`s to the C code so that we have access to the functions we want to test. We must also create a directory and copy the files to that directory. We will choose to do this in the subfolder `A2B` relative to this tutorial.

```python
%%writefile -a $out_dir/A2B_unit_test.c
#include "driver_AtoB.h" // This file contains both functions we need.

#include "calculate_exact_BU.h"
#include "calculate_AD.h"

```

    Appending to Validation//A2B_unit_test.c

Now, we'll write the main method. First, we'll set up the grid. In this test, we cannot use only one point. As we are testing a three-point stencil, we can get away with a minimal $3 \times 3 \times 3$ grid. Then, we'll write the A fields. After that, we'll calculate the magnetic field two ways.

```python
%%writefile -a $out_dir/A2B_unit_test.c
int main(int argc, const char *argv[]) {
    paramstruct params;
#include "set_Cparameters_default.h"

    // Let the last argument be the test we're doing. 1 = coarser grid, 0 = finer grid.
    int do_quadratic_test = atoi(argv[4]);

    // Step 0c: Set free parameters, overwriting Cparameters defaults
    //          by hand or with command-line input, as desired.
#include "free_parameters.h"
#include "set_Cparameters-nopointer.h"

    // We'll define our grid slightly different from how we normally would. We let our outermost
    // ghostzones coincide with xxmin and xxmax instead of the interior of the grid. This means
    // that the ghostzone points will have identical positions so we can do convergence tests of them.    // Step 0d.ii: Set up uniform coordinate grids
    REAL *xx[3];
    xx[0] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS0);
    xx[1] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS1);
    xx[2] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS2);
    for(int j=0;j<Nxx_plus_2NGHOSTS0;j++) xx[0][j] = xxmin[0] + ((REAL)(j))*dxx0;
    for(int j=0;j<Nxx_plus_2NGHOSTS1;j++) xx[1][j] = xxmin[1] + ((REAL)(j))*dxx1;
    for(int j=0;j<Nxx_plus_2NGHOSTS2;j++) xx[2][j] = xxmin[2] + ((REAL)(j))*dxx2;

    for(int j=0;j<Nxx_plus_2NGHOSTS0;j++) printf("x[%d] = %.5e\n",j,xx[0][j]);

    //for(int i=0;i<Nxx_plus_2NGHOSTS0;i++) printf("xx[0][%d] = %.15e\\n",i,xx[0][i]);

    // This is the array to which we'll write the NRPy+ variables.
    REAL *auxevol_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS2 * Nxx_plus_2NGHOSTS1 * Nxx_plus_2NGHOSTS0);
    REAL *evol_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS2 * Nxx_plus_2NGHOSTS1 * Nxx_plus_2NGHOSTS0);

    for(int i2=0;i2<Nxx_plus_2NGHOSTS2;i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS1;i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
        //auxevol_gfs[IDX4S(GAMMADETGF,i0,i1,i2)] = 1.0; // Flat Space
        auxevol_gfs[IDX4S(GAMMADETGF,i0,i1,i2)] = 1.0 - 1.0/(2.0+xx[0][i0]*xx[0][i0]+xx[1][i1]*xx[1][i1]+xx[2][i2]*xx[2][i2]);
    }

    // We now want to set up the vector potential. First, we must set the coefficients.
    if(is_gaussian) {
        // Gaussian coefficients:
        // Magnitudes:
        a = (double)(rand()%20)/5.0;
        f = (double)(rand()%20)/5.0;
        m = (double)(rand()%20)/5.0;
        // Offsets:
        b = (double)(rand()%10-5)/1000.0;
        c = (double)(rand()%10-5)/1000.0;
        d = (double)(rand()%10-5)/1000.0;
        g = (double)(rand()%10-5)/1000.0;
        h = (double)(rand()%10-5)/1000.0;
        l = (double)(rand()%10-5)/1000.0;
        n = (double)(rand()%10-5)/1000.0;
        o = (double)(rand()%10-5)/1000.0;
        p = (double)(rand()%10-5)/1000.0;
        /*printf("Offsets: b,c,d = %f,%f,%f\n",b,c,d);
        printf("Offsets: g,h,l = %f,%f,%f\n",g,h,l);
        printf("Offsets: n,o,p = %f,%f,%f\n",n,o,p);*/
    }
    else {
        // Polynomial coefficients
        // We will use random integers between -10 and 10. For the first test, we let the
        // Cubic coefficients remain zero. Those are a,b,c,g,h,l,p,q, and r.

        d = (double)(rand()%20-10);
        e = (double)(rand()%20-10);
        f = (double)(rand()%20-10);
        m = (double)(rand()%20-10);
        n = (double)(rand()%20-10);
        o = (double)(rand()%20-10);
        s = (double)(rand()%20-10);
        t = (double)(rand()%20-10);
        u = (double)(rand()%20-10);
    }

    if(do_quadratic_test && !is_gaussian) {
        calculate_AD(&params,xx,evol_gfs);

        // We'll also calculate the exact solution for B^i
        calculate_exact_BU(&params,xx,auxevol_gfs);

        // And now for the numerical derivatives:
        driver_A_to_B(&params,evol_gfs,auxevol_gfs);

        printf("This test uses quadratic vector potentials, so the magnetic fields should agree to roundoff error.\n");
        printf("Below, each row represents one point. Each column represents a component of the magnetic field.\n");
        printf("Shown is the number of Significant Digits of Agreement, at least 13 is good, higher is better:\n\n");
        for(int i2=0;i2<Nxx_plus_2NGHOSTS2;i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS1;i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
            printf("i0,i1,i2=%d,%d,%d; SDA: %.3f, %.3f, %.3f\n",i0,i1,i2,
                   1.0-log10(2.0*fabs(auxevol_gfs[IDX4S(B_ANALYTICU0GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU0GF,i0,i1,i2)])/(fabs(auxevol_gfs[IDX4S(B_ANALYTICU0GF,i0,i1,i2)])+fabs(auxevol_gfs[IDX4S(BU0GF,i0,i1,i2)])+1.e-15)),
                   1.0-log10(2.0*fabs(auxevol_gfs[IDX4S(B_ANALYTICU1GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU1GF,i0,i1,i2)])/(fabs(auxevol_gfs[IDX4S(B_ANALYTICU1GF,i0,i1,i2)])+fabs(auxevol_gfs[IDX4S(BU1GF,i0,i1,i2)])+1.e-15)),
                   1.0-log10(2.0*fabs(auxevol_gfs[IDX4S(B_ANALYTICU2GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU2GF,i0,i1,i2)])/(fabs(auxevol_gfs[IDX4S(B_ANALYTICU2GF,i0,i1,i2)])+fabs(auxevol_gfs[IDX4S(BU2GF,i0,i1,i2)])+1.e-15))
                  );
        }
    }

    if(!is_gaussian) {
        // Now, we'll set the cubic coefficients:
        a = (double)(rand()%20-10);
        b = (double)(rand()%20-10);
        c = (double)(rand()%20-10);
        g = (double)(rand()%20-10);
        h = (double)(rand()%20-10);
        l = (double)(rand()%20-10);
        p = (double)(rand()%20-10);
        q = (double)(rand()%20-10);
        r = (double)(rand()%20-10);
    }

    // And recalculate on our initial grid:
    calculate_AD(&params,xx,evol_gfs);

    // We'll also calculate the exact solution for B^i
    calculate_exact_BU(&params,xx,auxevol_gfs);

    // And now for the numerical derivatives:
    driver_A_to_B(&params,evol_gfs,auxevol_gfs);

    char filename[100];
    sprintf(filename,"out%d-numer.txt",Nxx0);
    FILE *out2D = fopen(filename, "w");
    if(do_quadratic_test || is_gaussian) {
        for(int i2=0;i2<Nxx_plus_2NGHOSTS2;i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS1;i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
            // We print the difference between approximate and exact numbers.
            fprintf(out2D,"%.16e\t%.16e\t%.16e %e %e %e\n",
                    auxevol_gfs[IDX4S(B_ANALYTICU0GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU0GF,i0,i1,i2)],
                    auxevol_gfs[IDX4S(B_ANALYTICU1GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU1GF,i0,i1,i2)],
                    auxevol_gfs[IDX4S(B_ANALYTICU2GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU2GF,i0,i1,i2)],
                    xx[0][i0],xx[1][i1],xx[2][i2]
                    );
        }
    }
    else {
        for(int i2=0;i2<Nxx_plus_2NGHOSTS2;i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS1;i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
            if (i0%2==0 && i1%2==0 && i2%2==0) {
                // We print the difference between approximate and exact numbers.
                fprintf(out2D,"%.16e\t%.16e\t%.16e %e %e %e\n",
                        auxevol_gfs[IDX4S(B_ANALYTICU0GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU0GF,i0,i1,i2)],
                        auxevol_gfs[IDX4S(B_ANALYTICU1GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU1GF,i0,i1,i2)],
                        auxevol_gfs[IDX4S(B_ANALYTICU2GF,i0,i1,i2)]-auxevol_gfs[IDX4S(BU2GF,i0,i1,i2)],
                        xx[0][i0],xx[1][i1],xx[2][i2]
                       );
            }
        }

    }
    fclose(out2D);
}

```

    Appending to Validation//A2B_unit_test.c

## Step 2.a: Compile and run the code

$$\label{compile_run}$$

Now that we have our file, we can compile it and run the executable.

```python
import time

print("Now compiling, should take ~2 seconds...\n")
start = time.time()
cmd.C_compile(os.path.join(out_dir,"A2B_unit_test.c"), os.path.join(out_dir,"A2B_unit_test"))
end = time.time()
print("Finished in "+str(end-start)+" seconds.\n\n")

print("Now running...\n")
start = time.time()
!./Validation/A2B_unit_test 1 1 1 0
# To do a convergence test, we'll also need a second grid with twice the resolution.
!./Validation/A2B_unit_test 7 7 7 1
!./Validation/A2B_unit_test 19 19 19 1
end = time.time()
print("Finished in "+str(end-start)+" seconds.\n\n")

```

    Now compiling, should take ~2 seconds...

    Compiling executable...
    Executing `gcc -Ofast -fopenmp -march=native -funroll-loops Validation/A2B_unit_test.c -o Validation/A2B_unit_test -lm`...
    Finished executing in 0.8151066303253174 seconds.
    Finished compilation.
    Finished in 0.8266806602478027 seconds.

    Now running...

    dxx0,dxx1,dxx2 = 3.33333e-03,3.33333e-03,3.33333e-03
    x[0] = -1.00000e-02
    x[1] = -6.66667e-03
    x[2] = -3.33333e-03
    x[3] = 4.33681e-19
    x[4] = 3.33333e-03
    x[5] = 6.66667e-03
    x[6] = 1.00000e-02
    dxx0,dxx1,dxx2 = 1.66667e-03,1.66667e-03,1.66667e-03
    x[0] = -1.00000e-02
    x[1] = -8.33333e-03
    x[2] = -6.66667e-03
    x[3] = -5.00000e-03
    x[4] = -3.33333e-03
    x[5] = -1.66667e-03
    x[6] = 4.33681e-19
    x[7] = 1.66667e-03
    x[8] = 3.33333e-03
    x[9] = 5.00000e-03
    x[10] = 6.66667e-03
    x[11] = 8.33333e-03
    x[12] = 1.00000e-02
    dxx0,dxx1,dxx2 = 8.33333e-04,8.33333e-04,8.33333e-04
    x[0] = -1.00000e-02
    x[1] = -9.16667e-03
    x[2] = -8.33333e-03
    x[3] = -7.50000e-03
    x[4] = -6.66667e-03
    x[5] = -5.83333e-03
    x[6] = -5.00000e-03
    x[7] = -4.16667e-03
    x[8] = -3.33333e-03
    x[9] = -2.50000e-03
    x[10] = -1.66667e-03
    x[11] = -8.33333e-04
    x[12] = 4.33681e-19
    x[13] = 8.33333e-04
    x[14] = 1.66667e-03
    x[15] = 2.50000e-03
    x[16] = 3.33333e-03
    x[17] = 4.16667e-03
    x[18] = 5.00000e-03
    x[19] = 5.83333e-03
    x[20] = 6.66667e-03
    x[21] = 7.50000e-03
    x[22] = 8.33333e-03
    x[23] = 9.16667e-03
    x[24] = 1.00000e-02
    Finished in 0.4406452178955078 seconds.

# Step 3: Code validation: Verify that relative error in numerical solution converges to zero at the expected order \[Back to [top](#toc)\]

$$\label{convergence}$$

Now that we have shown that when we use a quadratic vector potential, we get roundoff-level agreement (which is to be expected, since the finite-differencing used approximates the underlying function with a quadratic), we will use do a convergence test to show that when we can't exactly model the function, the truncation error dominates and converges to zero at the expected rate. For this, we use cubic functions for the vector potential. In the code above, we output the difference beteween the numeric and exact magnetic fields at the overlapping, non-edge, non-vertex points of two separate grids. Here, we import that data and calculate the convergence in the usual way,
$$
k = \log_2 \left( \frac{F - F_1}{F - F_2} \right),
$$
where $k$ is the convergence order, $F$ is the exact solution, $F_1$ is the approximate solution on the coarser grid with resolution $\Delta x$, and $F_2$ is the approximate solution on the finer grid with resolution $\Delta x/2$.

**TODO:**
* Plot: Abs. error of $B^i$ on X axis (incl. ghosts)
    * plot Low, High, Rescaled low
    * Repeat for each axis.
* Recheck convergence for cubic on interior?

L2 Norm:
$$
| B^i_{\rm approx} - B^i_{\rm exact}| = \sqrt{\frac{1}{N} \sum_{ijk} \left( B^i_{\rm approx} - B^i_{\rm exact} \right)^2}
$$

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

Data1 = np.loadtxt("out1-numer.txt")
Data2 = np.loadtxt("out7-numer.txt")

# print("Convergence test: All should be approximately 2\n")
# convergence = np.log(np.divide(np.abs(Data1),np.abs(Data2)))/np.log(2)
# for i in range(len(convergence[:,0])):
#    print(convergence[i,:])

def IDX4(i,j,k,Nxx_plus_2NGHOSTS0,Nxx_plus_2NGHOSTS1,Nxx_plus_2NGHOSTS2):
    return (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (0) ) )
comp = 0 # 0->Bx, 1->By, 2->Bz

# First, let's do this over the interior
N = 7 # This is the number of total gridpoints
nface = 0 # This is the number of points we are taking the norm of.
nint = 0 # This is the number of points we are taking the norm of.
L2_1 = 0
L2_1_xm = 0 # We declare one L2 norm for each face.
L2_1_xp = 0
L2_1_ym = 0
L2_1_yp = 0
L2_1_zm = 0
L2_1_zp = 0
for k in range(N):
    for j in range(N):
        for i in range(N):
            if i==0:
                L2_1_xm += Data1[IDX4(i,j,k,N,N,N),comp]**2
                nface += 1
            if i==N-1:
                L2_1_xp += Data1[IDX4(i,j,k,N,N,N),comp]**2
            if j==0:
                L2_1_ym += Data1[IDX4(i,j,k,N,N,N),comp]**2
            if j==N-1:
                L2_1_yp += Data1[IDX4(i,j,k,N,N,N),comp]**2
            if k==0:
                L2_1_zm += Data1[IDX4(i,j,k,N,N,N),comp]**2
            if k==N-1:
                L2_1_zp += Data1[IDX4(i,j,k,N,N,N),comp]**2
            if not (i%(N-1)==0 or j%(N-1)==0 or k%(N-1)==0):
                L2_1 += Data1[IDX4(i,j,k,N,N,N),comp]**2
                nint += 1

L2_1 = np.sqrt(L2_1/(nint))
L2_1_xm = np.sqrt(L2_1_xm/(nface))
L2_1_xp = np.sqrt(L2_1_xp/(nface))
L2_1_ym = np.sqrt(L2_1_ym/(nface))
L2_1_yp = np.sqrt(L2_1_yp/(nface))
L2_1_zm = np.sqrt(L2_1_zm/(nface))
L2_1_zp = np.sqrt(L2_1_zp/(nface))

N = 13 # This is the number of total gridpoints
nface = 0 # This is the number of points we are taking the norm of.
nint = 0 # This is the number of points we are taking the norm of.
L2_2 = 0
L2_2_xm = 0
L2_2_xp = 0
L2_2_ym = 0
L2_2_yp = 0
L2_2_zm = 0
L2_2_zp = 0
for k in range(N):
    for j in range(N):
        for i in range(N):
            if i==0:
                L2_2_xm += Data2[IDX4(i,j,k,N,N,N),comp]**2
                nface += 1
            if i==N-1:
                L2_2_xp += Data2[IDX4(i,j,k,N,N,N),comp]**2
            if j==0:
                L2_2_ym += Data2[IDX4(i,j,k,N,N,N),comp]**2
            if j==N-1:
                L2_2_yp += Data2[IDX4(i,j,k,N,N,N),comp]**2
            if k==0:
                L2_2_zm += Data2[IDX4(i,j,k,N,N,N),comp]**2
            if k==N-1:
                L2_2_zp += Data2[IDX4(i,j,k,N,N,N),comp]**2
            if not (i%(N-1)==0 or j%(N-1)==0 or k%(N-1)==0):
                L2_2 += Data2[IDX4(i,j,k,N,N,N),comp]**2
                nint += 1

L2_2 = np.sqrt(L2_2/(nint))
L2_2_xm = np.sqrt(L2_2_xm/(nface))
L2_2_xp = np.sqrt(L2_2_xp/(nface))
L2_2_ym = np.sqrt(L2_2_ym/(nface))
L2_2_yp = np.sqrt(L2_2_yp/(nface))
L2_2_zm = np.sqrt(L2_2_zm/(nface))
L2_2_zp = np.sqrt(L2_2_zp/(nface))

print("Face | Res  |  L2 norm  | Conv. Order")
print(" Int | Dx   | " + "{:.7f}".format(L2_1) + " | -- ")
print(" --  | Dx/2 | " + "{:.7f}".format(L2_2) + " | " + "{:.5f}".format(np.log2(L2_1/L2_2)))
print(" -x  | Dx   | " + "{:.7f}".format(L2_1_xm) + " | -- ")
print(" --  | Dx/2 | " + "{:.7f}".format(L2_2_xm) + " | " + "{:.5f}".format(np.log2(L2_1_xm/L2_2_xm)))
print(" +x  | Dx   | " + "{:.7f}".format(L2_1_xp) + " | -- ")
print(" --  | Dx/2 | " + "{:.7f}".format(L2_2_xp) + " | " + "{:.5f}".format(np.log2(L2_1_xp/L2_2_xp)))
print(" -y  | Dx   | " + "{:.7f}".format(L2_1_ym) + " | -- ")
print(" --  | Dx/2 | " + "{:.7f}".format(L2_2_ym) + " | " + "{:.5f}".format(np.log2(L2_1_ym/L2_2_ym)))
print(" +y  | Dx   | " + "{:.7f}".format(L2_1_yp) + " | -- ")
print(" --  | Dx/2 | " + "{:.7f}".format(L2_2_yp) + " | " + "{:.5f}".format(np.log2(L2_1_yp/L2_2_yp)))
print(" -z  | Dx   | " + "{:.7f}".format(L2_1_zm) + " | -- ")
print(" --  | Dx/2 | " + "{:.7f}".format(L2_2_zm) + " | " + "{:.5f}".format(np.log2(L2_1_zm/L2_2_zm)))
print(" +z  | Dx   | " + "{:.7f}".format(L2_1_zp) + " | -- ")
print(" --  | Dx/2 | " + "{:.7f}".format(L2_2_zp) + " | " + "{:.5f}".format(np.log2(L2_1_zp/L2_2_zp)))

```

    Face | Res  |  L2 norm  | Conv. Order
     Int | Dx   | 0.1055447 | --
     --  | Dx/2 | 0.0278600 | 1.92159
     -x  | Dx   | 0.1626309 | --
     --  | Dx/2 | 0.0380858 | 2.09427
     +x  | Dx   | 0.1540552 | --
     --  | Dx/2 | 0.0361027 | 2.09327
     -y  | Dx   | 0.1598659 | --
     --  | Dx/2 | 0.0509759 | 1.64898
     +y  | Dx   | 0.3187191 | --
     --  | Dx/2 | 0.0858693 | 1.89207
     -z  | Dx   | 0.1834849 | --
     --  | Dx/2 | 0.0425960 | 2.10687
     +z  | Dx   | 0.1584009 | --
     --  | Dx/2 | 0.0397512 | 1.99451

Results at higher resolution for comparison w/ Gaussian vector potential:
Face | Res  |  L2 norm  | Conv. Order
 Int | Dx   | 0.0817002 | --
 --  | Dx/2 | 0.0225991 | 1.85408
 -x  | Dx   | 0.1475312 | --
 --  | Dx/2 | 0.0344243 | 2.09952
 +x  | Dx   | 0.1475312 | --
 --  | Dx/2 | 0.0344243 | 2.09952
 -y  | Dx   | 0.2427657 | --
 --  | Dx/2 | 0.0698011 | 1.79824
 +y  | Dx   | 0.2427657 | --
 --  | Dx/2 | 0.0698011 | 1.79824
 -z  | Dx   | 0.1624672 | --
 --  | Dx/2 | 0.0405504 | 2.00236
 +z  | Dx   | 0.1624672 | --
 --  | Dx/2 | 0.0405504 | 2.00236

# Step 4: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]

$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-Start_to_Finish-GiRaFFE_NRPy-A2B.pdf](Tutorial-Start_to_Finish-GiRaFFE_NRPy-A2B.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
!jupyter nbconvert --to latex --template latex_nrpy_style.tplx --log-level='WARN' Tutorial-Start_to_Finish-GiRaFFE_NRPy-A2B.ipynb
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-GiRaFFE_NRPy-A2B.tex
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-GiRaFFE_NRPy-A2B.tex
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-GiRaFFE_NRPy-A2B.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode


# Curso Macroeconomia
# Prof. Cássio Besarria

```python
# Importando o numpy e nomeando-o

import numpy as np
    #importando o numpy (junto com todos seus módulos) e o nomenando como np
    #os módulos em default são: random, fft, lib, linalg, testing e core

# Importanto todos os objetos públicos do numpy dentro do namespace corrente

from numpy import *

# Importanto funções do numpy de módulos especificios

from numpy.random import rand, randn
    #importando as funções rand e randn do módulo random do numpy
    # rand = retorna números aleatórios dentro dentro de um formato
    # randn = retorna uma matriz com dados com base em uma distribuição normal padronizada
from numpy.fft import fft, ifft
    #importando as funções fft e ifft do módulo fft do numpy
    # fft = calcula a unidimensional transformação discreta de Fourier
    # ifft = calcula a unidimensional transformação discreta inversa de Fourier

# Demais importações

from scipy.stats import beta
    #Importanto a função beta do módulo stats do scipy
    # beta = Uma variável aleatória contínua beta

import matplotlib.pyplot as plt
    #importando o módulo(sub-pacote) pyplot do matplotlib e o nomenando como plt

```

```python
from sympy import Symbol, solve
    #Importa Symbol representa a possibilidade de operações com símbolos
    #Importa solve permite a solução de equações
```

```python
from pylab import plot, arange, legend
    #Importa a função de elaboração de gráficos da biblioteca pylab
    #Função plot para gerar gráficos
    #Função arange para definir as faixas de valores, o numpy possui essa função
    #Inclui legenda
```

```python
x = np.arange(0,4,0.5)
    #Usando a função arange do numpy
    #x fica no intervalo entre 0 e 4 (domínio) e vai variar de 0.5 entre os pontos
```

```python
#Plot 1: usando pylab e matplotlib

y = 0.8*x + 3
plot(x,y, 'xg:', label='Eq_1')
plt.axhline(ls='--', c='k')
    #Inclui a linha do eixo 'x'
legend(loc='best')
    #Escolhe a melhor localização pra legenda
```

```python
#Plot 3: usando matplotlib

plt.plot(x,y,'rx--')
#plt.title('Soluções da Eq_1')
plt.ylabel('Consumo (C)')
plt.xlabel('Renda disponível (Y)')
#plt.legend(['Eq_1'])
plt.annotate('Inclinação', xy=(2, 4.6), xytext=(1, 1.5),
            arrowprops=dict(facecolor='black', shrink=0.05),
            )
plt.axhline(ls='--', c='k')
plt.savefig('FuncaoConsumo')
plt.show()
```

```python
x = np.arange(0,1500,0.1)
    #Usando a função arange do numpy
    #x fica no intervalo entre 0 e 4 (domínio) e vai variar de 0.5 entre os pontos
```

```python
#Plot 1: usando pylab e matplotlib

y = x
z = 0.6*x + 400
plt.plot(x,z, color='blue', linewidth=2.5, linestyle="--", label='Demanda')
plot(x,y, 'r--', label='Oferta')
#plt.ylabel('Demanda (Z), Produção (Y)')
plt.xlabel('Renda (Y)')
plt.annotate(r'Equilíbrio',
            xy=(1000, 1000), xycoords='data',
            xytext=(-90, -90), textcoords='offset points', fontsize=10,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.annotate(r'Gasto autônomo',
            xy=(0, 400), xycoords='data',
            xytext=(50, 70), textcoords='offset points', fontsize=10,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.axhline(ls='--', c='k')
# Inclui a linha do eixo 'x'
legend(loc='best')
plt.savefig('Equilibrio')
```

# Alteração do gasto autônomo

```python
x = np.arange(0,2000,0.1)
    #Usando a função arange do numpy
    #x fica no intervalo entre 0 e 4 (domínio) e vai variar de 0.5 entre os pontos
```

```python
#Plot 1: usando pylab e matplotlib

y = x
z = 0.6*x + 400
w = 0.6*x + 600
plt.plot(x,z, color='blue', linewidth=2.5, linestyle="--", label='$D_0$')
plt.plot(x,w, color='green', linewidth=2.5, linestyle="--", label='$D_1$')
plot(x,y, 'r--', label='Oferta')
#plt.ylabel('Demanda (Z), Produção (Y)')
plt.xlabel('Renda (Y)')
plt.annotate(r'$Y_0=1000$',
            xy=(1000, 1000), xycoords='data',
            xytext=(-60, -90), textcoords='offset points', fontsize=10,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.annotate(r'$Y_1=1500$',
            xy=(1500, 1500), xycoords='data',
            xytext=(-60, -90), textcoords='offset points', fontsize=10,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.axhline(ls='--', c='k')
# Inclui a linha do eixo 'x'
legend(loc='best')
plt.savefig('Equilibrio_novo')
```

```python
x = np.arange(0,3500,0.1)
    #Usando a função arange do numpy
    #x fica no intervalo entre 0 e 4 (domínio) e vai variar de 0.5 entre os pontos
```

# Equilíbrio Modelo Keynesiano

```python
y = x
z = 0.8*x + 300
s = 0.2*x - 100
i = 0*x   + 200

plt.figure(figsize=(6, 4))
plt.subplot(2, 1, 1)
plt.plot(x,z, color='blue', linewidth=2.5, linestyle="--", label='Demanda')
plot(x,y, 'r--', label='Oferta')
#plt.ylabel('Demanda (Z), Produção (Y)')
plt.xlabel('Renda (Y)')
plt.annotate(r'Equilíbrio',
            xy=(1500, 1500), xycoords='data',
            xytext=(-10, 20), textcoords='offset points', fontsize=10,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.axhline(ls='--', c='k')
legend(loc='best')
# Inclui a linha do eixo 'x'
#plt.xticks(())
#plt.yticks(())
#plt.text(0.5, 0.5, 'subplot(2,1,1)', ha='center', va='center', size=24, alpha=.5)

plt.subplot(2, 1, 2)
plt.plot(x,s, color='green', linewidth=2.5, linestyle="--", label='Poupança')
plot(x,i, 'r', label='Investimento')
#plt.ylabel('Demanda (Z), Produção (Y)')
plt.xlabel('Renda (Y)')
plt.axhline(ls='--', c='k')

# Inclui a linha do eixo 'x'
#plt.xticks(())
#plt.yticks(())
legend(loc='best')
plt.tight_layout()
plt.savefig('Equilibrio_poupanca')
plt.show()
```

# Exemplo de gráfico

```python
import matplotlib.lines as lines
lines.lineStyles.keys()
```

    dict_keys(['-', '--', '-.', ':', 'None', ' ', ''])

```python
import matplotlib.markers as markers
markers.MarkerStyle.markers.keys()
```

    dict_keys(['.', ',', 'o', 'v', '^', '<', '>', '1', '2', '3', '4', '8', 's', 'p', '*', 'h', 'H', '+', 'x', 'D', 'd', '|', '_', 'P', 'X', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 'None', None, ' ', ''])

```python
plt.xlim(-4.0, 4.0) #define os limites em x
plt.xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi],
          [r'$-\pi$', r'$-\pi/2$', r'$0$', r'$+\pi/2$', r'$+\pi$']) #marcadores específicos

plt.ylim(-1.0, 1.0) #define os limites em y
plt.yticks([-1, 0, +1],
          [r'$-1$', r'$0$', r'$+1$']) #marcadores com nomes

ax = plt.gca()  # gca significa 'get current axis'
ax.spines['right'].set_color('none') #removendo o fechamento da direita
ax.spines['top'].set_color('none') #removendo o fechamento de cima
ax.xaxis.set_ticks_position('bottom') #mantendo os marcadores abaixo
ax.spines['bottom'].set_position(('data',0)) #movendo o limite inferior para 0 dos dados
ax.yaxis.set_ticks_position('left') #mantendo os marcadores à esquerda
ax.spines['left'].set_position(('data',0)) #movendo o limite da esquerda para 0 dos dados
```

```python
t = 2 * np.pi / 3
plt.plot([t, t], [0, np.cos(t)], color='blue', linewidth=2.5, linestyle="--")
plt.scatter([t, ], [np.cos(t), ], 50, color='blue')

plt.annotate(r'$sin(\frac{2\pi}{3})=\frac{\sqrt{3}}{2}$',
            xy=(t, np.sin(t)), xycoords='data',
            xytext=(+30, +5), textcoords='offset points', fontsize=16,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

plt.plot([t, t],[0, np.sin(t)], color='red', linewidth=2.5, linestyle="--")
plt.scatter([t, ],[np.sin(t), ], 50, color='red')

plt.annotate(r'$cos(\frac{2\pi}{3})=-\frac{1}{2}$',
            xy=(t, np.cos(t)), xycoords='data',
            xytext=(-90, -50), textcoords='offset points', fontsize=16,
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))

for label in ax.get_xticklabels() + ax.get_yticklabels():
    label.set_fontsize(16)
    label.set_bbox(dict(facecolor='white', edgecolor='None', alpha=0.65))
```

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

theta = np.linspace(-np.pi, np.pi, 30, endpoint=True)    # coordenadas de theta
y1 = np.cos(theta) # valores de y no cos
y2 = np.sin(theta) # valores de y no sen

# Cria uma figura de tamanho 8x6 pontos, 80 pontos por polegada
plt.figure(figsize=(8, 6), dpi=80)

# Cria um novo sub-gráfico a partir de um grid de 2x1 e posiciona no 1
plt.subplot(2, 1, 1)
plt.plot(theta, y1, 'g', label=r'$cos(\theta)$', linewidth=5) #linha vermelha com espessura de 5 em pontos

# Cria um novo sub-gráfico a partir de um grid de 2x1 e posiciona no 2
plt.subplot(2, 1, 2)
plt.plot(theta, y2, 'b-o', label=r'$sen(\theta)$') #azul com círculos
```

```python

```