$$
\newcommand{\dt}{\Delta t}
\newcommand{\udt}[1]{u^{({#1})}(T)}
\newcommand{\Edt}[1]{E^{({#1})}}
\newcommand{\uone}[1]{u_{1}^{({#1})}}
$$

This is the third in a series of posts on testing scientific software. For this to make sense, you'll need to have skimmed [the motivation and background](http://ianhawke.github.io/blog/close-enough.html). The [first in the series](http://ianhawke.github.io/blog/close-enough-part-1.html) assumed we only cared about the answer, and whether we'd implemented the expected algorithm wasn't important. The [second](http://ianhawke.github.io/blog/close-enough-part-2.html) assumed we cared about the convergence rate of the algorithm, not the specific answer, nor the precise algorithm itself.

In the previous cases we've focused on the *behaviour* of the algorithm: whether it will give the correct answer in the limit, or whether it converges as expected.  This is really what you want to do: you're trying to do science, to get an answer, and so implementing the precise algorithm should be secondary.  If you are trying to implement a precise algorithm, it should be because of its (expected) behaviour, and so you should be testing for that!

However, let's put that aside and see if we can work out how to test whether we've implemented exactly the algorithm we want: Euler's method. Checking convergence alone is not enough: the [Backwards Euler method](http://en.wikipedia.org/wiki/Backward_Euler_method) has identical convergence behaviour, as do whole families of other methods. We need a check that characterizes the method uniquely.

The *local truncation error* $\Edt{\dt}$ would be exactly such a check. This is the error produced by a single step from exact data, eg

$$
\begin{equation}
  \Edt{\dt} = u_1 - u(\dt).
\end{equation}
$$

This is enough as Euler's method is independent of the data, so each individual step performs the same operations.

For Euler's method we have

$$
\begin{equation}
  u_{n+1} = u_n + \dt f(t_n, u_n)
\end{equation}
$$

and so

$$
\begin{equation}
  \Edt{\dt} = \left| u_0 + \dt f(0, u_0) - u(\dt) \right| =  \left| \frac{\dt^2}{2} \left. u''\right|_{t=0} \right| + {\cal O}(\dt^3).
\end{equation}
$$

This is all well and good, but we don't know the exact solution (in principle) at any point other than $t=0$, so cannot compute $u(\dt)$, so cannot compute $\Edt{\dt}$. We only know $\uone{\dt}$ for whichever values of $\dt$ we wish to compute.

We can use repeated Richardson extrapolation to get the solution $u(\dt)$ to sufficient accuracy, however. On the *assumption* that the algorithm is first order (we can use the previous techniques to check this), we can use Richardson extrapolation to repeatedly remove the highest order error terms. We can thus find the local truncation errors.

```python
from math import sin, cos, log, ceil
import numpy
from matplotlib import pyplot
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

We will again need the code implementing Euler's method [the full phugoid model notebook](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/01_phugoid/01_03_PhugoidFullModel.ipynb).

```python
# model parameters:
g = 9.8      # gravity in m s^{-2}
v_t = 30.0   # trim velocity in m s^{-1}
C_D = 1/40.  # drag coefficient --- or D/L if C_L=1
C_L = 1.0    # for convenience, use C_L = 1

### set initial conditions ###
v0 = v_t     # start at the trim velocity (or add a delta)
theta0 = 0.0 # initial angle of trajectory
x0 = 0.0     # horizotal position is arbitrary
y0 = 1000.0  # initial altitude
```

```python
def f(u):
    """Returns the right-hand side of the phugoid system of equations.

    Parameters
    ----------
    u : array of float
        array containing the solution at time n.

    Returns
    -------
    dudt : array of float
        array containing the RHS given u.
    """

    v = u[0]
    theta = u[1]
    x = u[2]
    y = u[3]
    return numpy.array([-g*sin(theta) - C_D/C_L*g/v_t**2*v**2,
                      -g*cos(theta)/v + g/v_t**2*v,
                      v*cos(theta),
                      v*sin(theta)])
```

```python
def euler_step(u, f, dt):
    """Returns the solution at the next time-step using Euler's method.

    Parameters
    ----------
    u : array of float
        solution at the previous time-step.
    f : function
        function to compute the right hand-side of the system of equation.
    dt : float
        time-increment.

    Returns
    -------
    u_n_plus_1 : array of float
        approximate solution at the next time step.
    """

    return u + dt * f(u)
```

This time we will need lots of solutions in order to measure anything. We will construct ten local truncation errors. For each, we take a single step and store the result for $v$. Then, for each single step result, we use eight other calculations using our algorithm. Each will take multiple, smaller, steps to get a more accurate result for $v$ at the same time. We then use repeated Richardson extrapolation to find, to very high accuracy, the "true" result for $v$ at this time. Finally, we can compare against the original single step results to find the local truncation errors.

This *only* works if the algorithm is converging at the expected rate, as we checked in [the second post](http://ianhawke.github.io/blog/close-enough-part-2.html). It does not rely on the algorithm being exactly Euler's method (thankfully, or the argument would be circular!), but does need the convergence rate to be known.

```python
T_values = numpy.array([0.001*2**(i) for i in range(10)])
lte_values = numpy.zeros_like(T_values)
for j, T in enumerate(T_values):
    dt_values = numpy.array([T*2**(i-8) for i in range(8)])
    v_values = numpy.zeros_like(dt_values)
    for i, dt in enumerate(dt_values):
        N = int(T/dt)+1
        t = numpy.linspace(0.0, T, N)
        u = numpy.empty((N, 4))
        u[0] = numpy.array([v0, theta0, x0, y0])
        for n in range(N-1):
            u[n+1] = euler_step(u[n], f, dt)
        v_values[i] = u[-1,0]
    v_next = v_values
    for s in range(1, len(v_values-1)):
        v_next = (2**s*v_next[1:]-v_next[0:-1])/(2**s-1)
    lte_values[j] = abs(v_values[0]-v_next)
```

This gives us a set of local truncation errors at given timesteps:

```python
for dt, lte in zip(T_values, lte_values):
    print("For dt={} the local truncation error is {}.".format(dt, lte))
```

    For dt=0.001 the local truncation error is 1.99954897084e-09.
    For dt=0.002 the local truncation error is 8.05573563412e-09.
    For dt=0.004 the local truncation error is 3.26825286834e-08.
    For dt=0.008 the local truncation error is 1.34407251551e-07.
    For dt=0.016 the local truncation error is 5.67045063349e-07.
    For dt=0.032 the local truncation error is 2.50349015474e-06.
    For dt=0.064 the local truncation error is 1.18961298661e-05.
    For dt=0.128 the local truncation error is 6.26360622817e-05.
    For dt=0.256 the local truncation error is 0.000370836831557.
    For dt=0.512 the local truncation error is 0.00244291651282.

We now have many values for the local truncation error. We can thus compute the convergence rate of the local truncation error itself (which should be two), and check that it is close enough to the expected value using the [same techniques as in the second post in the series](http://ianhawke.github.io/blog/close-enough-part-2.html):

```python
s_m = numpy.zeros(2)
for i in range(2):
    s_m[i] = log(abs((lte_values[2+i]-lte_values[1+i])/
                     (lte_values[1+i]-lte_values[0+i]))) / log(2.0)
    print("Measured convergence rate (base dt {}) is {:.6g} (error is {:.4g}).".format(
            T_values[i], s_m[i], abs(s_m[i]-2)))
print("Convergence error has reduced by factor {:.4g}.".format(
        abs(s_m[0]-2)/abs(s_m[1]-2)))
```

    Measured convergence rate (base dt 0.001) is 2.02375 (error is 0.02375).
    Measured convergence rate (base dt 0.002) is 2.04637 (error is 0.04637).
    Convergence error has reduced by factor 0.5121.

So the error has gone down considerably, and certainly $0.51 > 1/3$, so the convergence rate of the local truncation error is close enough to 2.

However, that alone isn't enough to determine that this really is Euler's method: as noted above, the convergence rate of the local truncation error isn't the key point: the key point is that we can predict its *actual value* as

$$
\begin{equation}
  \Edt{\dt} =  \frac{\dt^2}{2} \left| \left. u''\right|_{t=0} \right| + {\cal O}(\dt^3) = \frac{\dt^2}{2} \left| \left( \left. \frac{\partial f}{\partial t} \right|_{t=0} + f(0, u_0) \left. \frac{\partial f}{\partial u} \right|_{t=0, u=u_0} \right) \right|.
\end{equation}
$$

For the specific problem considered here we have

$$
\begin{equation}
  u = \begin{pmatrix} v \\ \theta \\ x \\ y \end{pmatrix}, \quad f = \begin{pmatrix} -g\sin \theta - \frac{C_D}{C_L} \frac{g}{v_t^2} v^2 \\ -\frac{g}{v}\cos \theta + \frac{g}{v_t^2} v \\ v \cos \theta \\ v \sin \theta \end{pmatrix}.
\end{equation}
$$

We note that $f$ does not explicitly depend on $t$ (so $\partial f / \partial t \equiv 0$), and that the values of the parameters $g, C_D, C_L$ and $v_t$ are given above, along with the initial data $u_0 = (v_0, \theta_0, x_0, y_0)$.

So, let's find what the local truncation error should be.

```python
import sympy
sympy.init_printing()
v, theta, x, y, g, CD, CL, vt, dt = sympy.symbols('v, theta, x, y, g, C_D, C_L, v_t, {\Delta}t')
u = sympy.Matrix([v, theta, x, y])
f = sympy.Matrix([-g*sympy.sin(theta)-CD/CL*g/vt**2*v**2,
                  -g/v*sympy.cos(theta)+g/vt**2*v,
                  v*sympy.cos(theta),
                  v*sympy.sin(theta)])
dfdu = f.jacobian(u)
lte=dt**2/2*dfdu*f
```

```python
lte_0=lte.subs([(g,9.8),(vt,30.0),(CD,1.0/40.0),(CL,1.0),(v,30.0),(theta,0.0),(x,0.0),(y,1000.0)])
lte_0
```

So let us check the local truncation error values, which are computed for `v`:

```python
lte_exact = float(lte_0[0]/dt**2)
lte_values/T_values**2
```

    array([ 0.00199955,  0.00201393,  0.00204266,  0.00210011,  0.00221502,
            0.00244481,  0.00290433,  0.003823  ,  0.00565852,  0.00931899])

These are indeed converging towards $0.002 \dt^2$ as they should. To check this quantitatively, we use that our model is

$$
\begin{equation}
  \Edt{\dt} = \alpha \dt^2 + {\cal O}(\dt^3),
\end{equation}
$$

with the exact value $\alpha_e \simeq 0.002$. So we can use our usual Richardson extrapolation methods applied to $\Edt{\dt}/\dt^2$, to get a measured value for $\alpha$ with an error interval:

$$
\begin{equation}
  \alpha_m  = \frac{8\Edt{\dt} - \Edt{2\dt} \pm \left| \Edt{\dt} - \Edt{2\dt} \right|}{4\dt^2}.
\end{equation}
$$

```python
for i in range(len(lte_values)-1):
    Edt = lte_values[i]
    E2dt = lte_values[i+1]
    dt = T_values[i]
    err1 = abs(Edt - E2dt)
    a_lo = (8.0*Edt - E2dt - err1)/(4.0*dt**2)
    a_hi = (8.0*Edt - E2dt + err1)/(4.0*dt**2)
    print("Base dt={:.4g}: the measured alpha is in [{:.5g}, {:.5g}]".format(
            dt, a_lo, a_hi))
    print("Does this contain the exact value? {}".format(
            a_lo <= lte_exact <= a_hi))
```

    Base dt=0.001: the measured alpha is in [0.00047112, 0.0034992]
    Does this contain the exact value? True
    Base dt=0.002: the measured alpha is in [0.00044604, 0.0035244]
    Does this contain the exact value? True
    Base dt=0.004: the measured alpha is in [0.00039575, 0.0035747]
    Does this contain the exact value? True
    Base dt=0.008: the measured alpha is in [0.00029522, 0.0036752]
    Does this contain the exact value? True
    Base dt=0.016: the measured alpha is in [9.4165e-05, 0.0038763]
    Does this contain the exact value? True
    Base dt=0.032: the measured alpha is in [-0.00030782, 0.0042784]
    Does this contain the exact value? True
    Base dt=0.064: the measured alpha is in [-0.0011113, 0.0050826]
    Does this contain the exact value? True
    Base dt=0.128: the measured alpha is in [-0.0027153, 0.0066903]
    Does this contain the exact value? True
    Base dt=0.256: the measured alpha is in [-0.0059063, 0.0099024]
    Does this contain the exact value? True

So, to the limits that we can measure the local truncation error, we have implemented Euler's method.


# Circuit Quantum Electrodynamics

## Contents

1. [Introduction](#intro)
2. [The Schrieffer-Wolff Transformation](#tswt)
3. [Block-diagonalization of the Jaynes-Cummings Hamiltonian](#bdotjch)
4. [Full Transmon](#full-transmon)
5. [Qubit Drive with cQED](#qdwcqed)
6. [The Cross Resonance Entangling Gate](#tcreg)

## 1. Introduction

By analogy with Cavity Quantum Electrodynamics (CQED), circuit QED (cQED) exploits the fact that a simple model can be used to both describe the interaction of an atom with an optical cavity and a qubit with a microwave resonator. This model includes the number of photons in the cavity/resonator, the state of the atom/qubit, and the electric dipole interaction between the atom/qubit and cavity/resonator. As we saw in the last section, transmons are actually multi-level systems, but restricting ourselves to the ground $|0\rangle = |g\rangle$ and first excited $|1\rangle = |e\rangle$ states is possible because of the anharmonicity of the transmon. Therefore we can describe the transmon as a qubit described by the Pauli spin matrices
$$
\sigma^x = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} \qquad
\sigma^y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix} \qquad
\sigma^z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} \qquad
$$
that generate rotations the respective axes around the Bloch sphere. In that case, the simplest model to describe this interaction is the Jaynes-Cummings Hamiltonian in the rotating wave approximation,
$$
H_{\rm JC}^{\rm (RWA)}/\hbar = \omega_r a^\dagger a + \frac{1}{2} \omega_q \sigma_z + g(a^\dagger \sigma^- + a \sigma^+).
$$
where $\omega_r$ and $\omega_q$ are the frequencies of the resonator and "qubit", respectively, $a$ ($a^\dagger$) is the resonator photon annihilation (creation) operator, and $g$ is the electric dipole coupling (half the vacuum Rabi splitting). Note that we are now omitting the hats from the operators. Here, the first term corresponds to the number of photons in the resonator, the second term corresponds to the state of the qubit, and the third is the electric dipole interaction, where $\sigma^\pm = (1/2)(\sigma^x \mp i\sigma^y)$ is the qubit raising/lowering operator. (Note that the signs are inverted from those of *spin* raising/lowering operators, as discussed in the previous chapter).

This Hamiltonian can be solved exactly, and the solutions are hybrid qubit/resonator states where an excitation (either a photon in the resonator or excited state of the qubit) swaps between the two at a rate $g$ when they are on-resonance ($\omega_r = \omega_q$). For example, the $a^\dagger \sigma^-$ in the third term creates a photon in the resonator and lowers the qubit from $|1\rangle$ to $|0\rangle$, while the $a\sigma^+$ term destroys a photon in the resonators and excites the qubit from $|0\rangle$ to $|1\rangle$. While interesting, for our quantum computer we want to deal with qubits, and not these hybrid states. This means we want to move to a regimes where the resonator acts as a perturbation to the qubit (and vice-versa), so that their properties merely become "dressed" by the presence of the other. Using a type of perturbation theory, called the Schrieffer-Wolff (S-W) transformation, we can calculate the properties of the qubit and resonator in the regime we wish to operate. Here it should be noted that treating the transmon as a qubit is illustrative for pedagogical reasons, but the same techniques apply when you consider all the levels of the transmon. The higher levels of the transmon have profound effects and must be considered when designing and simulating them.

## 2. The Schrieffer-Wolff Transformation

    Schrödinger's Equation (Click here to expand)
Problems in quantum mechanics are often that of diagonalizing a Hamiltonian eigenvalue equation
$$
H\psi_m = E_m \psi_m \qquad {\rm for} \quad 1 \le m \le n
$$
where the $\psi_m$ are the eigenstates with eigenvalue $E_m$. This consists of finding a unitary matrix $U$, such that $H' = U H U^\dagger$ is diagonal. Then the eigenvalue equation
$$
\hat{H} \psi_m = E_m \psi_m \Longrightarrow U H U^\dagger U \psi_m = E_m U \psi_m \Longrightarrow H' \psi_m' = E_m \psi_m'
$$
where $\psi_m' = U\psi_m$ are the transformed eigenstates and
$$
H' = \begin{pmatrix}
E_1 & 0 & \cdots & 0 \\
0 & E_2 & \cdots & 0 \\
\vdots & \vdots & \ddots & 0 \\
0 & 0 & \cdots & E_n \end{pmatrix}
$$
is the diagonalized Hamiltonian.

With the S-W transformation, instead of diagonalizing the Hamiltonian, we seek to *block-diagonalize* it. Suppose we have a Hamiltonian that can be broken up into a diagonal part and perturbation
$$
H \quad = \quad \underbrace{\begin{pmatrix}
\Box &  &  &  &  &  &  \\
 & \Box &  &  &  &  &  \\
 &  & \Box &  &  &  &  \\
 &  &  & \Box &  &  &  \\
 &  &  &  & \Box &  &  \\
 &  &  &  &  & \Box &  \\
 &  &  &  &  &  & \Box \end{pmatrix}}_\text{diagonal} \quad + \quad
\underbrace{\begin{pmatrix}
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\times & \times & \times & \times & \cdot & \cdot & \cdot \\
\cdot & \cdot & \cdot & \cdot & \times & \times & \times \\
\cdot & \cdot & \cdot & \cdot & \times & \times & \times \\
\cdot & \cdot & \cdot & \cdot & \times & \times & \times \end{pmatrix}}_\text{perturbation}
$$
and then write the perturbation as $H_1 + H_2$ so that $H = H_0 + H_1 + H_2$, with $H_0$ diagonal, $H_1$ block-diagonal, and $H_2$ non-block diagonal, and we have
$$
H \quad = \quad \underbrace{\begin{pmatrix}
\Box &  &  &  &  &  &  \\
 & \Box &  &  &  &  &  \\
 &  & \Box &  &  &  &  \\
 &  &  & \Box &  &  &  \\
 &  &  &  & \Box &  &  \\
 &  &  &  &  & \Box &  \\
 &  &  &  &  &  & \Box \end{pmatrix}}_\text{diagonal}
\quad + \quad
\underbrace{\begin{pmatrix}
\times & \times & \times & \times &  &  &  \\
\times & \times & \times & \times &  &  &  \\
\times & \times & \times & \times &  &  &  \\
\times & \times & \times & \times &  &  &  \\
 &  &  &  & \times & \times & \times \\
 &  &  &  & \times & \times & \times \\
 &  &  &  & \times & \times & \times \end{pmatrix}}_\text{block diagonal}
\quad + \quad
\underbrace{\begin{pmatrix}
 &  &  &  & \cdot & \cdot & \cdot \\
 &  &  &  & \cdot & \cdot & \cdot \\
 &  &  &  & \cdot & \cdot & \cdot \\
 &  &  &  & \cdot & \cdot & \cdot \\
\cdot & \cdot & \cdot & \cdot &  &  &  \\
\cdot & \cdot & \cdot & \cdot &  &  &  \\
\cdot & \cdot & \cdot & \cdot &  &  &  \end{pmatrix}}_\text{block off-diagonal}
$$

Block-diagonalizing $H$ consists of finding an operator $S$ such that

$$
H_{\rm eff} = e^{iS} H e^{-iS} = \sum_{m=0}^\infty \frac{1}{m!} [H, S]^{(m)} = \sum_{m=0}^\infty \lambda^m H^{(m)},
$$

where $H^{(m)}$ are successive approximations to $H$ (with $H^{(0)} = H_0$) and the generalized commutator is defined recursively as

$$
[H,S]^{(m)} = [[H,S]^{(m-1)},S] \qquad {\rm with} \qquad [H,S]^{(0)} = H.
$$

Here we treat $S$ as a Taylor series with

$$ S = \sum_{m=1}^\infty \lambda^m S^{(m)} $$

to keep track of the order $\lambda$. Then expanding the effective Hamiltonian as a perturbation of $H_1+H_2$ to second order in $\lambda$,

$$
H_{\rm eff} = H_0 + \lambda (H_1+H_2) + \left[H_0 + \lambda(H_1+H_2), \lambda S^{(1)}\right]
  + \frac{1}{2} \left[ \left[ H_0 + \lambda(H_1+H_2), \lambda S^{(1)}\right], \lambda S^{(1)}\right]
  + \left[H_0 + \lambda(H_1+H_2), \lambda^2 S^{(2)}\right] + \ldots \\
  \approx H_0 + \lambda \left( H_1 + H_2 + \left[H_0, S^{(1)}\right] \right)
  + \lambda^2 \left( \left[H_1+H_2, S^{(1)}\right] + \frac{1}{2} \left[ \left[H_0, S^{(1)}\right], S^{(1)}\right] + \left[H_0, S^{(2)}\right]\right)
$$

Now we know $S$ must be block off-diagonal and anti-hermitian to force the block off-diagonal elements of $H_{\rm eff}$ to vanish, we must have that

$$
H_{\rm eff}^{\rm off-diag} = \sum_{m=0}^\infty \frac{1}{(2m+1)!} [\underbrace{H_0 + H_1}_\text{block diag}, S]^{(2m+1)} + \sum_{m=0}^\infty \frac{1}{(2m)!} [\underbrace{H_2}_\text{block off-diag}, S]^{(2m)} \equiv 0,
$$

noting that all the terms in the first series are block off-diagonal and all of those in the second series are block diagonal. This is because the commutator of a block diagonal and block off-diagonal matrix is block off-diagonal and the commutator of two block off-diagonal matrices is block diagonal. Expanding this to the generalized commutator, we can see that $[H^0 + H^1, S]^{(n)}$ with odd $n$ must always be block off-diagonal as well as $[H^2, S]^{(n)}$ with even $n$. Now expanding the off-diagonal part of the Hamiltonian to second order yields

$$
H_{\rm eff}^{\rm off-diag} = \left[ H_0 + \lambda H_1, \lambda S^{(1)} \right]+\lambda H_2 + \left[H_0 + \lambda H_1, \lambda^2 S^{(2)}\right]
  + \frac{1}{3!} \left[ H_0+\lambda H_1, \lambda S^{(1)}\right]^{(3)} + \frac{1}{2!} \left[ \lambda H_2, \lambda S^{(1)}\right]^{(2)} \\
  = \lambda \left( \left[ H_0, S^{(1)} \right] + H_2 \right) + \lambda^2 \left( \left[H_1, S^{(1)} \right] + \left[H_0, S^{(2)}\right]\right) + \ldots.
$$
Since each order of $\lambda$ must be identically zero, the following equations determine $S^{(m)}$,
$$
[H_0, S^{(1)}] = -H_2 \qquad
[H_0, S^{(2)}] = -[H_1, S^{(1)}] \qquad
[H_0, S^{(3)}] = -[H_1, S^{(2)}] - \frac{1}{3} [[H_2, S^{(1)}], S^{(1)}],
$$
where our ansatz that satisfied these equations is guaranteed unique by Winkler's work. Then our effective Hamiltonian becomes

$$
H_{\rm eff} = H_0+H_1+[H_2,S^{(1)}] + \frac{1}{2} [[H_0, S^{(1)}], S^{(1)}] + \ldots = H_0+H_1+\frac{1}{2}[H_2,S^{(1)}] + \ldots
$$

where the effective Hamiltonian is calculated here to second order and we have taken $\lambda \to 1$.

## 3. Block-diagonalization of the Jaynes-Cummings Hamiltonian

Using the S-W transformation consists of two problems: 1) finding the correct ansatz, and 2) performing the calculations. In most examples, an ansatz of similar form (i.e. anti-hermitian) to the off-diagonal parts is made and confirmed *a postori*. Recently, the manuscript [A Systematic Method for Schrieffer-Wolff Transformation and Its Generalizations](http://www.arxiv.org/abs/2004.06534) has appeared on the arXiv attesting to systematically providing the ansatz and applying it to numerous systems (including the Jaynes-Cumming Hamiltonian below).

As such, the *generator* $\eta$ is calculated as $\eta = [H_0, H_2]$. In keeping the scalar coefficients of $\eta$ undetermined, then $S^{(1)}$ can be calculated as the specific $\eta$ that satisfies $[H_0, \eta]=H_2$. Note the hermiticity of $H_0$ and $H_2$ guarantee the anti-hermiticity of $\eta$ and thus $S^{(1)}$.

For ease of tedious calculations, we will use the Python package [`sympy`](http://www.sympy.org) for symbolic mathematics.

```python
# import SymPy and define symbols
import sympy as sp
sp.init_printing(use_unicode=True)
wr = sp.Symbol('\omega_r') # resonator frequency
wq = sp.Symbol('\omega_q') # qubit frequency
g = sp.Symbol('g', real=True) # vacuum Rabi coupling
Delta = sp.Symbol('Delta', real=True) # wr - wq; defined later
```

```python
# import operator relations and define them
from sympy.physics.quantum.boson import BosonOp
a = BosonOp('a') # resonator photon annihilation operator
from sympy.physics.quantum import pauli, Dagger, Commutator
from sympy.physics.quantum.operatorordering import normal_ordered_form

# Pauli matrices
sx = pauli.SigmaX()
sy = pauli.SigmaY()
sz = pauli.SigmaZ()

# qubit raising and lowering operators, notice the spin and qubit ladder operators are inversed
splus = pauli.SigmaMinus()
sminus = pauli.SigmaPlus()
```

```python
# define J-C Hamiltonian in terms of diagonal and non-block diagonal terms
H0 = wr*Dagger(a)*a - (1/2)*wq*sz;
H2 = g*(Dagger(a)*sminus + a*splus);
HJC = H0 + H2; HJC # print
```

```python
# using the above method for finding the ansatz
eta = Commutator(H0, H2); eta
```

As a note about `sympy`, we will need to used the methods doit(), `expand`, `normal_ordered_form`, and `qsimplify_pauli` to proceed with actually taking the commutator, expanding it into terms, normal ordering the bosonic modes (creation before annihilation), and simplify the Pauli algebra. Trying this with $\eta$ yields

```python
pauli.qsimplify_pauli(normal_ordered_form(eta.doit().expand()))
```

Now take $A$ and $B$ as the coefficients of $a^\dagger \sigma_-$ and $a\sigma_+$, respectively. Then the commutator

```python
A = sp.Symbol('A')
B = sp.Symbol('B')
eta = A * Dagger(a) * sminus - B * a * splus;
pauli.qsimplify_pauli(normal_ordered_form(Commutator(H0, eta).doit().expand()))
```

This expression should be equal to $H_2$

```python
H2
```

which implies $A = B = g/\Delta$ where $\Delta = \omega_r - \omega_q$ is the frequency detuning between the resonator and qubit. Therefore our $S^{(1)}$ is determined to be

```python
S1 = eta.subs(A, g/Delta)
S1 = S1.subs(B, g/Delta); S1.factor()
```

Then we can calculate the effective second order correction to $H_0$

```python
Heff = H0 + 0.5*pauli.qsimplify_pauli(normal_ordered_form(Commutator(H2, S1).doit().expand())).simplify(); Heff
```

This is typically written as
$$
H_{\rm eff} = \frac{g^2}{2\Delta} \left(\omega_r + \frac{g^2}{\Delta}\sigma_z\right)a^\dagger a  - \frac{1}{2}\left(\omega_q -\frac{g^2}{\Delta}\right) \sigma_z
$$
which shows a state-dependent shift by $\chi \equiv g^2/\Delta$ of the resonator frequency called the *ac Stark shift* and a shift in qubit frequency due to quantum vacuum fluctuations called the *Lamb shift*.

## 4.  Full Transmon

Because we are using *transmons* instead of *qubits*, we need to be careful to take the higher-order energy terms into effect when designing and simulating devices. The full transmon Hamiltonian coupled to the readout resonators is

$$
H^{\rm tr} = \omega_r a^\dagger a + \sum_j \omega_j |j\rangle\langle j| + g\left(a^\dagger c + ac^\dagger \right),
$$

where now $c = \sum_j \sqrt{j+1}|j\rangle\langle j+1|$ is the transmon lowering operator. Similarly, taking the weakly interacting subsets $A$ as the even-numbered transmon modes and $B$ as the odd-numbered transmon modes. Using the ansatz

$$
S^{(1)} = \sum_j \alpha_j a^\dagger \sqrt{j+1}|j\rangle\langle j+1| - \alpha_j^* a \sqrt{j+1}|j+1\rangle\langle j|,
$$

one may proceed along a messier version of the Jaynes-Cummings Hamiltonian. With some effort one can show the second order effective Hamiltonian is

$$
H^{\rm tr}_{\rm eff} = \left( \omega_r + \sum_j \frac{g^2(\omega_r-\omega+\delta)}{(\omega_r-\omega-\delta j)(\omega_r - \omega - \delta(j-1))} |j\rangle\langle j| \right) a^\dagger a + \sum_j
\left[
j\omega + \frac{\delta}{2} (j-1)j + \frac{jg^2}{\omega-\omega_r+(j-1)\delta} \right]|j\rangle\langle j|.
$$

## 5. Qubit Drive with cQED

Following that of [Blais *et al* (2004)](https://arxiv.org/abs/cond-mat/0402216), we model the drive Hamiltonian as
$$
H^d(t) = \xi(t)\left( a^\dagger e^{-i\omega_d t} + ae^{i\omega_d t}\right).
$$
Following the treatment in the [Ph.D. dissertation of Lev Bishop](https://arxiv.org/abs/1007.3520), the drive acts on the qubit via the Glauber operator
$$
D(\alpha) = e^{\alpha(t) a^\dagger - \alpha^*(t) a}.
$$
Moving to the Jaynes-Cumming Hamiltonian rotating at the drive frequency,
$$
H = \Delta_r a^\dagger a - \frac{1}{2} \Delta_q \sigma^z + g(a^\dagger \sigma^- + a\sigma^+) + \xi(t)(a^\dagger + a)
$$
with $\Delta_r = \omega_r - \omega_d$ and $\Delta_q = \omega_q - \omega_d$. Applying Hadamard's Lemma to nested commutators,
$$
e^{A}BA^{-A} = B + [A,B] + \frac{1}{2!} [A,[A,B]] + \frac{1}{3!}[A,[A,[A,B]]] + \ldots
$$
we see that
$$
D^\dagger a^{(\dagger)} D = \exp\{-\alpha(t) a^\dagger + \alpha^*(t) a\} a^{(\dagger)}\exp\{\alpha(t) a^\dagger - \alpha^*(t) a\}
= a^{(\dagger)} + \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^{(\dagger)}\right] + \frac{1}{2!}\left[-\alpha(t) a^\dagger + \alpha^*(t) a, \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^{(\dagger)}\right]\right] + \ldots
 = a^{(\dagger)} + \alpha^{(*)}
$$
and

$$
D^\dagger a^\dagger a D = a^\dagger a + \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^\dagger a\right] + \frac{1}{2!}\left[-\alpha(t) a^\dagger + \alpha^*(t) a, \left[-\alpha(t) a^\dagger + \alpha^*(t) a, a^\dagger a\right]\right] + \ldots
= a^\dagger a + \alpha(t)a^\dagger + \alpha^*(t)a + |\alpha(t)|^2
$$

So that we can transform the Hamiltonian

$$
\tilde{H} = D^\dagger H D - iD^\dagger \dot{D} = \Delta_r\left(a^\dagger a + \alpha(t)a^\dagger + \alpha^*(t)a + |\alpha(t)|^2\right) - \frac{1}{2} \Delta_q \sigma^z \\ + g\left((a^\dagger + \alpha^*(t))\sigma^- + (a+\alpha(t))\sigma^+\right) + \xi(t)\left(a^\dagger + \alpha^*(t) + a + \alpha(t) \right) - i\left(\dot{\alpha}(t) a^\dagger - \dot{\alpha}^*(t) a\right) \\
= \Delta_r a^\dagger a - \frac{1}{2}\Delta_q \sigma^z + g\left((a^\dagger + \alpha^*(t))\sigma^- + (a+\alpha(t))\sigma^+\right) \\
+\xi(t)\left(a^\dagger  + a \right) + \Delta_r\left(\alpha(t)a^\dagger + \alpha^*(t)a\right)- i\left(\dot{\alpha}(t) a^\dagger - \dot{\alpha}^*(t) a\right)
$$

where the non-operator terms have been dropped. The last line can be set to zero if we choose

$$
-i\dot{\alpha}(t) + \Delta_r \alpha(t) + \xi(t) = 0,
$$

and finally introducing the Rabi frequency $\Omega(t) = 2g\alpha(t)$, we arrive at

$$
\tilde{H} = \Delta_r a^\dagger a - \frac{1}{2}\Delta_q \sigma^z + g\left(a^\dagger\sigma^- + a\sigma^+\right)
+\frac{1}{2} \left( \Omega^*(t)\sigma^- + \Omega(t) \sigma^+\right).
$$

Since the drive part of the Hamiltonian is block off-diagonal, we can perform a Schrieffer-Wolff transformation on it (for a real drive $\Omega^*(t) = \Omega(t)$) and add it to the effective Hamiltonian,

$$
[\tilde{H}^d, S^{(1)}] = -\frac{\Omega(t)}{2} \left[ (\sigma^- + \sigma^+),\frac{g}{\Delta}\left( a^\dagger \sigma^- - a\sigma^+\right)\right] = \frac{g\Omega(t)}{2\Delta}(a + a^\dagger)\sigma^z
$$

so the effective Hamiltonian becomes

$$
\tilde{H}_{\rm eff} = \left( \Delta_r + \frac{g^2}{\Delta}\sigma^z\right) a^\dagger a - \frac{1}{2}\left(\Delta_q - \frac{g^2}{\Delta}\right) \sigma^z + \frac{\Omega(t)}{2}\sigma^x
+ \frac{g\Omega(t)}{4\Delta}(a + a^\dagger)\sigma^z.
$$

Note here that to eliminate the $z$ rotations, one should drive at the Lamb-shifted qubit frequency. The additional $\sigma^z$ term is small because $\Delta \gg g$ in the dispersive regime.

## 6. The Cross Resonance Entangling Gate

Driving qubit one at the frequency of qubit two can be written as

$$
H^d(t) = \frac{\Omega(t)}{2} \left( \sigma_1^+ e^{-i\tilde{\omega}_2 t} + \sigma_1^- e^{i\tilde{\omega}_2 t}\right).
$$

Now, we need to apply Schrieffer-Wolff to the drive term to get the effective Hamiltonian, and then do the RWA at frequency $\tilde{\omega}_2$.

$$
[\tilde{H}^d, S^{(1)}] = -\frac{J\Omega(t)}{2\Delta_{12}} \left[ \sigma_1^+ e^{-i\tilde{\omega}_2 t} + \sigma_1^- e^{i\tilde{\omega}_2 t}, \sigma_1^+ \sigma_2^- - \sigma_2^+ \sigma_1^-\right]
=-\frac{J\Omega(t)}{2\Delta_{12}} \left(\sigma_1^z \sigma_2^+ e^{-i\tilde{\omega}_2 t}
+\sigma_1^z \sigma_2^- e^{i\tilde{\omega}_2 t} \right)
$$

Transforming back the rotating frame at $\omega_2$, we get the effective qubit cross resonance Hamiltonian

$$
\tilde{H}_{\rm eff}^{\rm CR} = - \frac{\tilde{\omega}_1-\tilde{\omega}_2}{2}\sigma_1^z
+ \frac{\Omega(t)}{2} \left(\sigma_2^x - \frac{J}{2\Delta_{12}} \sigma_1^z \sigma_2^x \right).
$$

The first two terms involve the $ZI$ interaction due to a Stark shift on qubit 1 and an unconditional $IX$ rotation on qubit 2, but the final term represents the $ZX$-interaction that produces entanglement. By putting qubit 1 into an equal superposition of $|0\rangle$ and $|1\rangle$ and applying the cross resonance gate for a duration corresponding to a $\pi/2$ rotation around the $x$-axis, a maximally entangled state is produced. Using Qiskit to characterize the two-qubit cross resonance Hamiltonian for transmons can be done with [this tutorial](https://github.com/Qiskit/qiskit-tutorials/blob/9405254b38312771f8d5c2dd6f451cec35307995/tutorials/noise/1_hamiltonian_and_gate_characterization.ipynb). Further reading on the cross resonance gate is found [here](https://arxiv.org/abs/1106.0553) and [here](https://arxiv.org/abs/1603.04821).


# Why You Should Hedge Beta and Sector Exposures (Part II)
by Jonathan Larkin and Maxwell Margenot

Part of the Quantopian Lecture Series:

* [www.quantopian.com/lectures](https://www.quantopian.com/lectures)
* [github.com/quantopian/research_public](https://github.com/quantopian/research_public)

Notebook released under the Creative Commons Attribution 4.0 License.

---

In the first lecture on [Why You Should Hedge Beta and Sector Exposure](quantopian.com/lectures/why-you-should-hedge-beta-and-sector-exposures-part-i), we covered the information coefficient (IC) and effective breadth, providing yet more reasons to make as many independent bets as possible. Here we expand upon the concepts detailed there by decomposing portfolios of varying numbers of securities to further explore the effects of systematic risk.

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

## Hedging Beta and Sector Risk is Good for Allocators (Which is Good for You!)

Let's work from two basic beliefs:
- You would like someone to fund your algorithm
- The institution that funds your algorithm is not going to allocate 100% of its money to you. In other words, your algorithm is one in a portfolio of algorithms.

The implication of the second belief is subtle. Why should it matter that your high Sharpe algo is part of a portfolio? The key to understanding the importance of this and what it has to do with beta and sector exposure is the following mathematical result:

**In a portfolio, stock specific risk can be diversified out while common factor risk cannot.**

TL;DR: Beta and sector exposure are **common factors**, i.e., they are among a handful of risk characteristics that are shared among all stocks. Risk exposure to common factors does not diversify away in a portfolio of algos. An allocator will not be able to make a large allocation to you if your algo presents common factor risk. The combination of many algos with modest common factor risk can lead to overwhelming common factor risk at the portfolio level. Allocators do not like this. If you want to get a large capital allocation, you must have low beta and sector exposure consistently over time.

# Foundations

### Single Stock Risk Decomposition

To build intuition, let's posit a single factor model:

$$r_i = \alpha_i + \beta_i r_m + \epsilon_i$$

where $\alpha_i$ is the intercept, $\epsilon_i$ is the error, and $r_m$ is the market return. This is the [Capital Asset Pricing Model (CAPM)](https://www.quantopian.com/lectures/the-capital-asset-pricing-model-and-arbitrage-pricing-theory), which posits that the returns to a stock can be attributable to its beta-weighted exposure to the market and a return which is idiosyncratic to that stock. Two important assumptions here are that the $\epsilon_i$s are uncorrelated to the market and each other across stocks. See the [Lecture on Beta Hedging](https://www.quantopian.com/lectures/beta-hedging) for more background.

In this case, the "risk", as measured by the variance, for an individual stock is:

$$\sigma_i^2 = \beta_i^2 \sigma_m^2 + \sigma_{\epsilon_i}^2$$

A stocks variance is broken into the **common risk**, $\beta_i^2\sigma_m^2$, and **specific risk**, $\sigma_{\epsilon_i}$. **Common risk** is risk in the stock driven by market risk which is common among all stocks proportionate to the stock's beta. **Specific risk** is the risk that is unique to that individual stock.

Let's look at two examples and decompose the risk into the percent due to common factor risk.

```python
def stock_risk(beta, market_vol, idio_vol):
    common_risk = (beta**2)*(market_vol**2)
    specific_risk = idio_vol**2
    total_risk = common_risk + specific_risk
    return total_risk, common_risk/total_risk
```

We take two separate stocks, each with different market beta exposures and idiosyncratic volatility.

```python
# Betas
b1 = 1.2
b2 = 1.1
# Market volatility
market_vol = 0.15
# Idiosyncratic volatilities
idio_vol_1 = 0.10
idio_vol_2 = 0.07
```

```python
total_1, pct_common_1 = stock_risk(b1, market_vol, idio_vol_1)
total_2, pct_common_2 = stock_risk(b2, market_vol, idio_vol_2)

print "Stock 1 risk (annualized standard deviation): %0.4f " % np.sqrt(total_1)
print "Stock 1: percent of total risk due to common risk: %0.4f " % pct_common_1

print "\nStock 2 risk (annualized standard deviation): %0.4f " % np.sqrt(total_2)
print "Stock 2: percent of total risk due to common risk: %0.4f " % pct_common_2
```

    Stock 1 risk (annualized standard deviation): 0.2059
    Stock 1: percent of total risk due to common risk: 0.7642

    Stock 2 risk (annualized standard deviation): 0.1792
    Stock 2: percent of total risk due to common risk: 0.8475

This is just looking at the breakdown of the risk associated with each individual stock. We can combine these into a portfolio to see how their combined volatility is affected by common factor risk.

### Two Stock Portfolio Risk Decomposition

Now let's imagine you have a two stock portfolio with percentage weights $w_1$ and $w_2$. The risk of the portfolio (derived below), $\Pi$, under the one-factor model is then:

$$\sigma_{\Pi}^2 = \overbrace{\sigma_m^2\left( w_1^2\beta_1^2 + w_2^2\beta_2^2 + 2w_1w_2\beta_1\beta_1 \right)}^{\text{common risk}} + \overbrace{w_1^2\epsilon_1^2 + w_2^2 \epsilon_2^2}^{\text{specifc risk}}$$

This is the simplest possible example of portfolio factor risk, one factor and two assets, yet we can already use it to gain intuition about portfolio risk and hedging.

```python
# The weights for each security in our portfolio
w1 = 0.5
w2 = 0.5
```

```python
def two_stocks_one_factor(w1, w2, b1, b2, market_vol, idio_vol_1, idio_vol_2):
    common_risk = (market_vol**2)*(w1*w1*b1*b1 + w2*w2*b2*b2 + 2*w1*w2*b1*b2)
    specific_risk =  w1*w1*idio_vol_1**2 + w2*w2*idio_vol_2**2
    total_risk = common_risk + specific_risk
    return total_risk, common_risk/total_risk
```

The risk for a two stock, equally-weighted, long-only portfolio:

```python
total, pct_common = two_stocks_one_factor(w1, w2, b1, b2, market_vol, idio_vol_1, idio_vol_2)
print "Portfolio risk (annualized standard deviation): %0.4f " % np.sqrt(total)
print "Percent of total risk due to common risk: %0.4f" % pct_common
```

    Portfolio risk (annualized standard deviation): 0.1830
    Percent of total risk due to common risk: 0.8887

The astute reader will notice that the proportion of risk in the portfolio due to common factor risk is **larger for the portfolio** than for the weighted sum of the common risk proportion for the two components. To repeat the key point in this lecture: **In a portfolio, stock specific risk diversifies while common factor risk does not.**

The risk for a two stock, beta-hedged long-short portfolio:

```python
w2 = -w1*b1/b2    # set weight 2 such that the portfolio has zero beta

total, pct_common = two_stocks_one_factor(w1, w2, b1, b2, market_vol, idio_vol_1, idio_vol_2)
print "Portfolio risk (annualized standard deviation): %0.4f " % np.sqrt(total)
print "Percent of total risk due to common risk: %0.4f" % pct_common
```

    Portfolio risk (annualized standard deviation): 0.0629
    Percent of total risk due to common risk: 0.0000

Note that we eliminated **all** the common risk with a perfect beta hedge.

# Portfolio Risk

If $X$ is a column vector of n random variables, $X_1,\dots,X_n$, and $c$ is a column vector of coefficients (constants), then the [variance of the weighted sum](https://en.wikipedia.org/wiki/Variance) $c'X$ is

$$\text{Var}(c'X) = c'\Sigma c$$

where $\Sigma$ is the covariance matrix of the $X$'s.

In our application, $c$ is our stock weight vector $w$ and $\Sigma$ is the covariance matrix of stock returns.

$$\sigma_{\Pi}^2 = w' \Sigma w$$

Just as we decompose the single stock risk above, we can decompose the covariance matrix to separate *common risk* and *specific risk*

$$\Sigma = BFB' + D$$

Thus

$$\sigma_{\Pi}^2  = w'(BFB' + D)w$$
$$\sigma_{\Pi}^2  = w'BFB'w + w'Dw$$

Which for the two stock portfolio above works out to

\begin{equation}
\sigma_{\Pi}^2 =
\overbrace{
\begin{bmatrix} w_1 & w_2 \end{bmatrix}
\begin{bmatrix} \beta_{1} \\ \beta_{2} \end{bmatrix}
\sigma_m^2
\begin{bmatrix} \beta_{1} & \beta_{2} \end{bmatrix}
\begin{bmatrix} w_1 \\ w_2 \end{bmatrix}
}^{\text{common risk}}
+ \overbrace{\begin{bmatrix} w_1 & w_2 \end{bmatrix}
\begin{bmatrix} \epsilon_1^2 & 0\\ 0 & \epsilon_2^2 \end{bmatrix}
\begin{bmatrix} w_1 \\ w_2 \end{bmatrix}}^{\text{specific risk}}
\end{equation}

If you work through this matrix multiplication, you get the stated result above

$$\sigma_{\Pi}^2 = \overbrace{\sigma_m^2\left( w_1^2\beta_1^2 + w_2^2\beta_2^2 + 2w_1w_2\beta_1\beta_1 \right)}^{\text{common risk}} + \overbrace{w_1^2\epsilon_1^2 + w_2^2 \epsilon_2^2}^{\text{specifc risk}}$$

### Multi-Factor Models

Of course, we can expand the CAPM to include *additional* risk factors besides market beta. We could posit that there are in total $m$ risks which are *common* to all stocks.

$$r_i = \alpha_i + \beta_{1,i} f_1 + \dots + \beta_{m,i} f_m + \epsilon_i$$

or more concisely

$$r_i = \alpha_i + \sum_{j=1}^m \beta_{j,i} f_j + \epsilon_i$$

or, considering all stocks, $i$, from 1 to N, even more concisely, for a given period $t$,

$$r = \alpha + Bf + \epsilon$$

where $r$ is the Nx1 column vector of returns, $B$ is the Nx$m$ matrix of factor betas, $f$ is the Nx1 column of factor returns, and $\epsilon$ is the Nx1 column vector of idiosyncratic returns. Finally,

$$\sigma_{\Pi}^2  = w'BFB'w + w'Dw$$

where $B$ is the Nx$m$ matrix of factor betas, $F$ is the $m$x$m$ covariance matrix of factor returns, and $D$ is a NxN matrix with the $\epsilon_i$'s on diagonal, and zeros everywhere else.

With this result, *assuming we had a suitable risk model giving us the matrices $B$, $F$, and $D$*, we could calculate our portfolio risk and the proportion of risk coming from common risk.

Likewise, just as we set $w_2$ above in the two stock case to the value that neutralized the exposure to the single factor $\beta$, in the multi-factor case we could use the factor betas matrix $B$ to construct a portfolio which is neutral to **all** common factors. **A portfolio which is neutral to all common factors has zero common factor risk.**

# Portfolios of Algos

Even without a risk model, we can get some intuition as to how the risk of a portfolio of algos looks.

What does a resulting portfolio of algos look like when the individual algos have non-zero common risk? Taking some inspiration from a recent journal article [The Dangers of Diversification](http://www.iijournals.com/doi/abs/10.3905/jpm.2017.43.2.013?journalCode=jpm) by Garvey, Kahn, and Savi, imagine that each algo has a certain *budget of common risk* it can take. This budget is defined as the percent common risk of total risk in the algo.

In the first case, we assume that all algos have this same budget (and use all the budget!) and the correlation between their common risks is 1.0. This is simular to the case of a single factor model.

```python
def portfolio_risk_decomposition(budget=0.2, correl=1.0, algo_count=2, algo_total_risk=0.04):
    N = algo_count

    algo_common_risk = budget*(algo_total_risk**2)
    algo_idio_risk = algo_total_risk**2 - algo_common_risk

    w = 1./N

    covar = correl*algo_common_risk
    common_risk = N*w*w*algo_common_risk + (N*N - N)*w*w*covar
    idio_risk = algo_idio_risk*w
    total_risk = common_risk + idio_risk

    return total_risk, common_risk/total_risk
```

```python
a, b = portfolio_risk_decomposition(budget=0.2, algo_count=20, correl=1.0, algo_total_risk=0.04)
print "Portfolio total risk: %.4f " % np.sqrt(a)
print "Portfolio percent of common risk: %.4f " % b
```

    Portfolio total risk: 0.0196
    Portfolio percent of common risk: 0.8333

```python
algos = np.linspace(1,20)
plt.plot(
    algos,
    portfolio_risk_decomposition(budget=0.2, correl=1.0, algo_count=algos)[1]
)
plt.plot(
    algos,
    portfolio_risk_decomposition(budget=0.4, correl=1.0, algo_count=algos)[1]
)
plt.ylim([0,1]);
plt.title('Percent of Portfolio Risk due to Common Risk')
plt.xlabel('Number of Algos in Portfolio')
plt.ylabel('Percent of Portfolio of Algos Risk due to Common Risk')
plt.legend(
    ['20% Single Algo Common Risk Budget', '40% Single Algo Common Risk Budget']
);
```

From this plot, you can see that from the allocator's perspective, a "small" budget that allows for 20% of individual algo total risk to be driven by common risk leads to a 20 algo portfolio **with 83%** of it's risk driven by common risk! Ideally an allocator wants you to have **zero common factor risk**.

TL;DR: Even if you can't predict portfolio risk and don't have a risk model to decompose risk, you can form a portfolio with **zero common risk** by hedging the beta exposure to common factors. The most important common factors in the US Equity market are market beta and sector beta. Hedge your beta and be sector neutral if you want a large allocation from any allocator.

*This presentation is for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation for any security; nor does it constitute an offer to provide investment advisory or other services by Quantopian, Inc. ("Quantopian"). Nothing contained herein constitutes investment advice or offers any opinion with respect to the suitability of any security, and any views expressed herein should not be taken as advice to buy, sell, or hold any security or as an endorsement of any security or company.  In preparing the information contained herein, Quantopian, Inc. has not taken into account the investment needs, objectives, and financial circumstances of any particular investor. Any views expressed and data illustrated herein were prepared based upon information, believed to be reliable, available to Quantopian, Inc. at the time of publication. Quantopian makes no guarantees as to their accuracy or completeness. All information is subject to change and may quickly become unreliable for various reasons, including changes in market conditions or economic circumstances.*


```python
from revenues import *
import copy
from multiprocessing import Pool, cpu_count
import sys
from sympy.core.numbers import Float

aStd, aFarming = var("aStd, aFarming")
# these symbols are declared here in order to differentiate standard
# operation from mining, because they have different ages.
eCostStd, eCostFarming, costStd, costFarming, RwStd = var("eCostStd, eCostFarming, costStd, costFarming, RwStd")
subs = [
    (eCostStd, eCost.subs(u, 1).subs(age, aStd)),
    (eCostFarming, eCost.subs(u, 0).subs(age, aFarming)),
    (costStd, cost.subs(age, aStd)),
    (costFarming, cost.subs(age, aFarming)),
    (RwStd, R_w.subs(age, aStd).subs(u, 1))
]
```

```python
# Busy POET
params = valSubs

##
# Three options:
# - useful work
# - standard mining
# - farming

# Useful work is in revenue.py
bestAgeUseful, revUseful = useful_work(params)

# Standard mining
# note we introduce overheadPoET to the cost of standard mining
perCPUCostStandard = (eCostStd + overheadStd + costStd)
perCPURev = (.9*RwStd + R_b - perCPUCostStandard)
cpuCountStandard = (total_cap / perCPUCostStandard).subs(params)
revStandard = (perCPURev * cpuCountStandard)

# Farming: (Mining in standard form doesn't make sense)
perCPUCostFarm = (eCostFarming + overheadFarm + costFarming)
perCPURevFarm = (R_b - perCPUCostFarm)
cpuCountFarm = (total_cap / perCPUCostFarm).subs(params)
revFarm = (perCPURevFarm * cpuCountFarm)

# equilibrium:
numStdMiner = var("numStdMiner")
numFarmer = var("numFarmer")
busyPoET_R_b = annualCryptoRev / ( numStdMiner * cpuCountStandard + numFarmer * cpuCountFarm )
busyPoET_R_b = busyPoET_R_b.subs(subs).subs(params)
```

```python
aSingleMiner = var("aSingleMiner")

singleMinerUtility = revStandard\
    .subs(subs)\
    .subs(aStd, aSingleMiner)\
    .subs(R_b, busyPoET_R_b)\
    .subs(params)

condAgeStdOptimal = diff(singleMinerUtility, aSingleMiner).subs(aSingleMiner, aStd)

singleFarmerUtility = revFarm\
    .subs(subs)\
    .subs(aFarming, aSingleMiner)\
    .subs(R_b, busyPoET_R_b)\
    .subs(params)

condAgeFarmingOptimal = diff(singleFarmerUtility, aSingleMiner).subs(aSingleMiner, aFarming)

condRevStdEqUseful = revStandard.subs(R_b,busyPoET_R_b).subs(subs).subs(params) - revUseful
condRevFarmingEqUseful = revFarm.subs(R_b,busyPoET_R_b).subs(subs).subs(params) - revUseful

pprint (condAgeStdOptimal.atoms(Symbol))
pprint (condAgeFarmingOptimal.atoms(Symbol))
pprint (condRevStdEqUseful.atoms(Symbol))
pprint (condRevFarmingEqUseful.atoms(Symbol))

print solve([condAgeStdOptimal, condAgeFarmingOptimal, condRevStdEqUseful, condRevFarmingEqUseful],
            [aFarming, aStd, numFarmer, numStdMiner])
```

```python

# fully expand now
    busyPoET_R_b = busyPoET_R_b.subs(subs)
    equiStdOpRatio = equiStdOpRatio.subs(subs)

    aF = MAX_STALENESS / 2
    aS = MAX_STALENESS / 2
    turn = 'S'
    ageStdDone = False
    ageFarmingDone = False

    while True:
        # print 'In turn %s aF=%.2f and aS=%.2f' % (turn, aF, aS)
        if turn == 'S' and not ageStdDone:
            tmp = aS
            # one guy works at aSS, others all work at aOthers
            # R_b and equiStdOpRatio is determined by aOthers
            f = lambda aOthers: aOthers - minimize_scalar(
                lambdify(aStd,
                        -(revStandard
                        .subs(R_b, busyPoET_R_b.subs(aStd, aOthers))
                        .subs(stdOpRatio, equiStdOpRatio.subs(aStd, aOthers))
                        .subs(subs).subs(params)
                        .subs(aFarming, aF))),
                bounds=(0, MAX_STALENESS),
                method='bounded').x

            aS = fsolve(f, MAX_STALENESS / 2)
            if abs(tmp - aS) < 0.01:
                ageStdDone = True
            turn = 'F'
        elif turn == 'F' and not ageFarmingDone:
            tmp = aF
            # one guy works at aFF, others all work at aOthers
            # R_b and equiStdOpRatio is determined by aOthers
            f = lambda aOthers: aOthers - minimize_scalar(
                    lambdify(aFarming,
                            -(revFarm.subs(R_b, busyPoET_R_b.subs(aFarming, aOthers))
                                .subs(stdOpRatio, equiStdOpRatio.subs(aFarming, aOthers))
                                .subs(subs).subs(params)
                                .subs(aStd, aS))),
                    bounds=(0, MAX_STALENESS),
                    method='bounded').x
            aF = fsolve(f, MAX_STALENESS / 2)
            if abs(tmp - aF) < 0.01:
                ageFarmingDone = True
            turn = 'S'
        else:
            break

    equiStdOpRatio_v = equiStdOpRatio.subs(subs).subs(params).subs(aStd, aS).subs(aFarming, aF)
    print 'Find equilibrium %.2f' % (equiStdOpRatio_v)

    if equiStdOpRatio_v <= 0:
        equiStdOpRatio_v = 0
        # if no standard mining at all
        f = lambda aOthers: aOthers - minimize_scalar(
                lambdify(aFarming,
                        -(revFarm
                        .subs(R_b, busyPoET_R_b.subs(aFarming, aOthers))
                        .subs(stdOpRatio, equiStdOpRatio_v)
                        .subs(subs)
                        .subs(params))),
                bounds=(0, MAX_STALENESS),
                method='bounded').x
        aF = fsolve(f, MAX_STALENESS / 2)[0]
        aS = -1
    elif equiStdOpRatio_v >= 1:
        equiStdOpRatio_v = 1
        f = lambda aOthers: aOthers - minimize_scalar(
                lambdify(aStd,
                        -(revStandard
                        .subs(R_b, busyPoET_R_b.subs(aStd, aOthers))
                        .subs(stdOpRatio, equiStdOpRatio_v)
                        .subs(subs)
                        .subs(params))),
                bounds=(0, MAX_STALENESS),
                method='bounded').x
        aS = fsolve(f, MAX_STALENESS / 2)[0]
        aF = -1

    print "Result: annualCryptoRev %.2f aStd %.2f ; aFarming %.2f equilibrium is %.2f" % (
        params["annualCryptoRev"], aS, aF, equiStdOpRatio_v)

    v_busypoet_Rb = busyPoET_R_b\
            .subs(stdOpRatio, equiStdOpRatio_v)\
            .subs(aFarming, aF)\
            .subs(aStd, aS)\
            .subs(params)

# if one guy is farming using an older chip
    # revStandard_v = revStandard\
    #         .subs(R_b, v_busypoet_Rb)\
    #         .subs(stdOpRatio, equiStdOpRatio_v)\
    #         .subs(subs)\
    #         .subs(params)\
    #         .subs(aStd, aS)\
    #         .subs(aFarming, aF)

    # revFarm_v = revFarm\
    #         .subs(R_b, v_busypoet_Rb)\
    #         .subs(stdOpRatio, equiStdOpRatio_v)\
    #         .subs(subs)\
    #         .subs(params)\
    #         .subs(aStd, aS)\
    #         .subs(aFarming, aF)

    # print("Busy PoET revStandard@%.2f: %s%.2f%s" % (aS, colors.BOLD, revStandard_v, colors.ENDC))
    # print("Busy PoET revFarm@%.2f: %s%.2f%s" % (aF, colors.BOLD, revFarm_v, colors.ENDC))

    usefulPrice = (total_cap / (equiStdOpRatio_v*cpuCountStandard*perfSlowdownExpression)
        ).subs(subs).subs(valSubs).subs(age, aS).subs(aStd, aS) / usefulPriceBaseline

    if equiStdOpRatio_v == 0:
# if no one is doing useful work
        usefulPrice = -1
        revFarm_10_v = revFarm.subs(R_b, v_busypoet_Rb).subs(stdOpRatio, equiStdOpRatio_v).subs(subs).subs(params).subs(aFarming, MAX_STALENESS)
        print("Busy PoET revFarm@%.2f: %s%.2f%s" % (MAX_STALENESS, colors.BOLD, revFarm_10_v, colors.ENDC))

    print 'Usefulprice is', usefulPrice
    return (params["annualCryptoRev"], params['operatorCount'], aS, aF, equiStdOpRatio_v, usefulPrice)
```

```python
def test(params):
    return params['annualCryptoRev']

def varyAnnaulCryptoRev(func, minRev, maxRev, n):
    params = []
    for an in np.linspace(minRev, maxRev, n):
        tmp = copy.copy(valSubs)
        tmp["annualCryptoRev"] = an
        params.append(tmp)

    pool = Pool(processes=cpu_count())
    results = np.array(pool.map(func, params))
    np.savetxt('../oakland/figs/%s-rev.txt' % func.__name__,
            results, fmt='%.4f',
            header='annualCryptoRev, operatorCount, aUseful, aStd, equil, usefulPrice')

def varyOperatorCount(func):
    params = []
    for an in np.linspace(10, 5000, 50, dtype=int):
        tmp = copy.copy(valSubs)
        tmp["operatorCount"] = float(an)
        params.append(tmp)

    pool = Pool(processes=cpu_count())
    results = np.array(pool.map(func, params))
    np.savetxt('../oakland/figs/%s-opcount.txt' % func.__name__,
            results, fmt='%.4f',
            header='annualCryptoRev, operatorCount, aUseful, aStd, equil, usefulPrice')

# if __name__ == '__main__':
#     # varyAnnaulCryptoRev(lazy_poet, 100000, 3000000, 50)
#     varyAnnaulCryptoRev(busy_poet, 100000, 3000000, 10)
```

```python
def busy_poet_rb(aS, aF, nS, nF):
    return busyPoET_R_b.subs(subs).subs(params).subs([
            (aStd, aS),
            (aFarming, aF),
            (numStdMiner, nS),
            (numFarmer, nF)])

def condRevEqual1(ageStd, ageFarm, numStd, numFarm):
    Rb = busy_poet_rb(ageStd, ageFarm, numStd, numFarm);
    return (revStandard - revUseful).subs(R_b, Rb).subs(subs).subs(params).subs([
            (aStd, ageStd),
            (aFarming, ageFarm),
            (numStdMiner, numStd),
            (numFarmer, numFarm)])

def condRevEqual2(ageStd, ageFarm, numStd, numFarm):
    Rb = busy_poet_rb(ageStd, ageFarm, numStd, numFarm);
    return (revFarm - revUseful).subs(R_b, Rb).subs(subs).subs(params).subs([
            (aStd, ageStd),
            (aFarming, ageFarm),
            (numStdMiner, numStd),
            (numFarmer, numFarm)])

def condOptimalAgeStd(ageStd, ageFarm, numStd, numFarm):
    Rb = busy_poet_rb(ageStd,ageFarm,numStd,numFarm)
#     print 'in condOptimalAgeStd: %.2f, %.2f, %.2f, %.2f, %.2f' % (Rb, ageStd,ageFarm,numStd,numFarm)
    r = abs(ageStd - minimize_scalar(
        lambda ageStdPrime: -1*revStandard.subs(R_b, Rb).subs(subs).subs(params).subs(aStd, ageStdPrime),
        bounds=(0, MAX_STALENESS),
        method='bounded').x)
#     print 'in condOptimalAgeStd: -> %.2f' % (r)
    return r

def condOptimalAgeFarm(ageStd, ageFarm, numStd, numFarm):
    Rb = busy_poet_rb(ageStd,ageFarm,numStd,numFarm)
#     print 'in condOptimalAgeFarm: %.2f, %.2f, %.2f, %.2f, %.2f' % (Rb, ageStd,ageFarm,numStd,numFarm)
    r = abs(ageFarm - minimize_scalar(
        lambda ageFarmPrime: -1*revFarm.subs(R_b, Rb).subs(subs).subs(params).subs(aFarming, ageFarmPrime),
        bounds=(0, MAX_STALENESS),
        method='bounded').x)

#     print 'in condOptimalAgeFarm: -> %.2f' % (r)
    return r

def conditions(x):
    ageStd, ageFarm, numStd, numFarm = x
    ageStd = min(10, max(0, ageStd))
    r = (condRevEqual1(ageStd, ageFarm, numStd, numFarm),
           condRevEqual2(ageStd, ageFarm, numStd, numFarm),
           condOptimalAgeStd(ageStd, ageFarm, numStd, numFarm),
           condOptimalAgeFarm(ageStd, ageFarm, numStd, numFarm))
    print '%.2f '*4 % tuple(x)
    return r
```

```python
fsolve(conditions, [5,5,10000,10000])
```

    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.41 96.96 -44512684.34 13578043.69
    4.76 164.57 -22797855.75 6611016.08
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.00 5.00 10000.00 10000.00
    5.41 96.96 -44512684.34 13578043.69
    4.76 164.57 -22797855.75 6611016.08
    4.61 208.11 -11896120.45 3123184.47
    4.51 233.22 -6376223.00 1364230.11
    4.46 242.97 -3477508.39 454422.77
    4.52 225.63 -1709404.61 -50187.79
    5.14 53.95 231451.72 -279472.47
    4.93 -19.48 -100725.86 154736.23

    /Users/fanz/Library/Python/2.7/lib/python/site-packages/scipy/optimize/minpack.py:236: RuntimeWarning: The iteration is not making good progress, as measured by the
      improvement from the last ten iterations.
      warnings.warn(msg, RuntimeWarning)

    array([  5.00000000e+00,   5.00000000e+00,   1.00000000e+04,
             1.00000000e+04])

```python

```


# Initial Data for Solving Maxwell's Equations in Flat Spacetime

## Authors: Terrence Pierre Jacques, Zachariah Etienne and Ian Ruchlin

## This module constructs the initial data for Maxwell's equations as symbolic (SymPy) expressions, for a purely toriodal dipole field, as defined in [Knapp, Walker & Baumgarte (2002)](https://arxiv.org/abs/gr-qc/0201051).

**Notebook Status:**  Validated

**Validation Notes:** All expressions generated in this module have been validated, against the [Dendro code Maxwell initial data](https://github.com/paralab/Dendro-GR), and have satisfied the contraints given in [Tutorial-VacuumMaxwell_Curvilinear_RHS-Rescaling](Tutorial-VacuumMaxwell_Curvilinear_RHS-Rescaling.ipynb), as well as the wave equation for the electric field and the vector potential.

### NRPy+ Source Code for this module: [Maxwell/InitialData.py](../edit/Maxwell/InitialData.py),  [reference_metric.py](../edit/reference_metric.py)

[comment]: <> (Introduction: TODO)

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

1. [Step 1](#initializenrpy): Initialize needed Python/NRPy+ modules and set destination basis
1. [Step 2](#step2): A Purely Toriodal Dipole Field
    1. [Step 2.a](#cart_basis): Converting to Cartesian Basis
    1. [Step 2.b](#dst_basis): Converting to Destination Basis
1. [Step 3](#step3): Checks
    1. [Step 3.a](#lorentz): Lorentz Gauge Condition & Divergence Constraint
    1. [Step 3.b](#wave_eq): Check that $A^i$ satisfies the wave equation
1. [Step 4](#step4): Code Validation
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Initialize needed Python/NRPy+ modules and set destination basis \[Back to [top](#toc)\]

$$\label{initializenrpy}$$

```python
# Import needed Python modules
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import reference_metric as rfm   # NRPy+: Reference metric support
import sympy as sp                # SymPy: The Python computer algebra package upon which NRPy+ depends

#Step 0: Set the spatial dimension parameter to 3.
par.set_parval_from_str("grid::DIM", 3)
DIM = par.parval_from_str("grid::DIM")

# Choices are: Spherical, SinhSpherical, SinhSphericalv2, Cylindrical, SinhCylindrical,
#              SymTP, SinhSymTP
dst_basis = "Cylindrical"

# To help with simplifications, we tell Sympy that
#  the coordinate xx0 is radial like (positive real)
radial_like_dst_xx0 = True

# Set coordinate system to Cartesian
par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
rfm.reference_metric()
```

# Step 2: A Purely Toriodal Dipole Field \[Back to [top](#toc)\]
$$\label{step2}$$

Having the evolution equations from [Knapp, Walker & Baumgarte (2002)](https://arxiv.org/abs/gr-qc/0201051) written in [Tutorial-VacuumMaxwell_Cartesian_RHSs](Tutorial-VacuumMaxwell_Cartesian_RHSs.ipynb), we must construct the initial data that will then be time evolved. Beginning from the analytic solution to this system of equation given by equation 16 of [Knapp, Walker & Baumgarte (2002)](https://arxiv.org/abs/gr-qc/0201051),

\begin{align}
A^{\hat{\phi}} &= \mathcal{A} \sin \theta \left( \frac{e^{-\lambda v^2}-e^{-\lambda u^2}}{r^2} - 2 \lambda \frac{ve^{-\lambda v^2}-ue^{-\lambda u^2}}{r} \right), \\
\end{align}

where $A^{\hat{\phi}} = A^{\phi} r\sin\theta$, $\mathcal{A}$ gives the amplitude, $\lambda$ describes the size of the wavepacket, $u = t+r$, and $v = t-r$. Other components of the vector potential are $0$. Note that these expressions repesent the exact solution to both systems of equations at any time $t \geq 0$, at all points on our numerical grid. Thus, to get initial data we set $t=0$.

For system II, we will also need to set initial data for $\Gamma$. Since $\Gamma = -\partial_t \psi$ and we have chosen $\psi(t=0) = 0$, $\Gamma(t=0) = 0$.

We may calculate $E^i$ using

\begin{align}
E^i = -\partial_t A^i.
\end{align}

**Inputs for initial data**:

* amp -  $A$
* lam - $\lambda$
* time - $t$

Below we define the Cartesian coordinates $x, y$ and $z$. We then define the vector potential $A^i$ in spherical coordinates, but each component is written in terms of Cartesian coordinates. This makes the subsequent basis changes easier.

```python
x = rfm.xx_to_Cart[0]
y = rfm.xx_to_Cart[1]
z = rfm.xx_to_Cart[2]

# Step 1: Declare free parameters intrinsic to these initial data
# Amplitude
amp = par.Cparameters("REAL",__name__,"amp", defaultvals=1.0)
# lambda
lam = par.Cparameters("REAL",__name__,"lam", defaultvals=1.0)
time = par.Cparameters("REAL",__name__,"time", defaultvals=0.0)
wavespeed = par.Cparameters("REAL",__name__,"wavespeed", defaultvals=1.0)

psi_ID = sp.sympify(0)

Gamma_ID = sp.sympify(0)
```

\begin{align}
A^{\hat{\phi}} &= \mathcal{A} \sin \theta \left( \frac{e^{-\lambda v^2}-e^{-\lambda u^2}}{r^2} - 2 \lambda \frac{ve^{-\lambda v^2}-ue^{-\lambda u^2}}{r} \right), \\
A^{\phi} &= \frac{A^{\hat{\phi}}} {r\sin\theta}
\end{align}

```python
AidU_Sph = ixp.zerorank1()
# Set coordinate transformations:
r = sp.sqrt(x*x + y*y + z*z)
sin_theta = z / r

u = time + r
v = time - r
e_lam_u = sp.exp(-lam*u**2)
e_lam_v = sp.exp(-lam*v**2)

# Equation 16 from https://arxiv.org/abs/gr-qc/0201051
AU_phi_hat = (amp*sin_theta)*( ((e_lam_v - e_lam_u)/r**2) - \
                        2*lam*(v*e_lam_v + u*e_lam_u)/r )

AidU_Sph[2] = AU_phi_hat/(r*sin_theta)
```

## Step 2.a: Converting to Cartesian Basis \[Back to [top](#toc)\]
$$\label{cart_basis}$$

Note that $A^i$ is defined in sperical coordinates, so we must therefore transform to Cartesian coordinates using the [Jacobian](https://en.wikipedia.org/wiki/Jacobian_matrix_and_determinant#Example_3:_spherical-Cartesian_transformation). Here we will use the coordinate transformation definitions provided by [reference_metric.py](../edit/reference_metric.py) to build the Jacobian:

\begin{align}
\frac{\partial x_{\rm Cart}^i}{\partial x_{\rm Sph}^j},
\end{align}

where $x_{\rm Sph}^j \in \{r,\theta,\phi\}$ and $x_{\rm Cart}^i \in \{x,y,z\}$. We then apply it to $A^i$ to transform into Cartesian coordinates, via

\begin{align}
A^i_{\rm Cart} = \frac{\partial x_{\rm Cart}^i}{\partial x_{\rm Sph}^j} A^j_{\rm Sph}.
\end{align}

```python
# Coordinate transformation from spherical to Cartesian
AidU_Cart = ixp.zerorank1()
Jac_dxSphU_dxCartD = ixp.zerorank2()
for i in range(DIM):
    for j in range(DIM):
        Jac_dxSphU_dxCartD[i][j] = sp.diff(rfm.xxSph[i],rfm.xx_to_Cart[j])

#         Jac_dxCartU_dxSphD[i][j] = sp.diff(rfm.xx_to_Cart[i],rfm.xx[j])
Jac_dxCartU_dxSphD,dummy = ixp.generic_matrix_inverter3x3(Jac_dxSphU_dxCartD)

for i in range(DIM):
    for j in range(DIM):
        AidU_Cart[i] += Jac_dxCartU_dxSphD[i][j]*AidU_Sph[j]
for i in range(DIM):
    AidU_Cart[i] = sp.simplify(AidU_Cart[i])

```

## Step 2.b: Converting to Destination Basis \[Back to [top](#toc)\]
$$\label{dst_basis}$$

Here we prepare to convert $A^i$ from the Cartesian basis to the destination basis. To do so, we first rewrite each component of $A^i$ in terms of the destination coordinates. This is done by first re-labelling the NRPy+ coordinates $xx0, xx1, xx2$ as $cart_{xx0}, cart_{xx1}, cart_{xx2}$. Then, each $cart_{xxi}$ is replaced by its counterpart expression in the destination basis using [reference_metric.py](../edit/reference_metric.py).

Note that for algebraic simplification, we tell sympy that the coordinate $xx0$ is radial like and thus positive and real (if the destination coordinates are curvilinear).

```python
# rfm is still defined in Cartesian coordinates
cart_xx = ixp.declarerank1("cart_xx")
for i in range(DIM):
    for k in range(DIM):
        AidU_Cart[i] = AidU_Cart[i].subs(rfm.xx[k], cart_xx[k])

# Set coordinate system to dst_basis
par.set_parval_from_str("reference_metric::CoordSystem",dst_basis)
rfm.reference_metric()

for i in range(DIM):
    for k in range(DIM):
        AidU_Cart[i] = AidU_Cart[i].subs(cart_xx[k], rfm.xx_to_Cart[k])

if radial_like_dst_xx0:
    for j in range(DIM):
        AidU_Cart[j] =  sp.refine(sp.simplify(AidU_Cart[j]), sp.Q.positive(rfm.xx[0]))

```

We define Jacobians relative to the center of the destination grid, at a point $x^j_{\rm dst}=$(`xx0,xx1,xx2`)${}_{\rm dst}$ on the destination grid:
$$
{\rm Jac\_dUCart\_dDdstUD[i][j]} = \frac{\partial x^i_{\rm Cart}}{\partial x^j_{\rm dst}},
$$

via exact differentiation (courtesy SymPy), and the inverse Jacobian
$$
{\rm Jac\_dUdst\_dDCartUD[i][j]} = \frac{\partial x^i_{\rm dst}}{\partial x^j_{\rm Cart}},
$$

using NRPy+'s `generic_matrix_inverter3x3()` function. In terms of these, the transformation of BSSN tensors from Cartesian to the destination grid's `"reference_metric::CoordSystem"` coordinates may be written:

$$
A^i_{\rm dst} = \frac{\partial x^i_{\rm dst}}{\partial x^\ell_{\rm Cart}} A^\ell_{\rm Cart}
$$

```python
# Step 3: Transform BSSN tensors in Cartesian basis to destination grid basis, using center of dest. grid as origin

# Step 3.a: Next construct Jacobian and inverse Jacobian matrices:
Jac_dUCart_dDrfmUD,Jac_dUrfm_dDCartUD = rfm.compute_Jacobian_and_inverseJacobian_tofrom_Cartesian()

# Step 3.b: Convert basis of all BSSN *vectors* from Cartesian to destination basis
AidU = rfm.basis_transform_vectorU_from_Cartesian_to_rfmbasis(Jac_dUrfm_dDCartUD, AidU_Cart)

# Define electric field --> E^i = -\partial_t A^i
EidU = ixp.zerorank1()
for j in range(DIM):
    EidU[j] = -sp.diff(AidU[j], time)
```

# Step 3: Checks \[Back to [top](#toc)\]
$$\label{step3}$$

Here we validate the initial data. Specifically, we check that the constraints from [Tutorial-VacuumMaxwell_formulation_Curvilinear](Tutorial-VacuumMaxwell_formulation_Curvilinear.ipynb) are satisfied;

\begin{align}
\mathcal{G} &\equiv \Gamma - \partial_i A^i - \hat{\Gamma}^i_{ji} A^j &= 0, \quad &\text{Lorenz gauge condition} \\
\mathcal{C} &\equiv \partial_i E^i + \hat{\Gamma}^i_{ji} E^j  &= 0, \quad &\text{Divergence Constraint}.
\end{align}

Note that the above simply to their usual forms in Cartesian coordinates.

Finally, we check that $A^i$ satisfies the covariant wave equation,

\begin{align}
\partial_t^2 A^i - \hat{\gamma}^{jk} \hat{\nabla}_j \hat{\nabla}_k A^i = 0,
\end{align}

where $\hat{\nabla}_j$ is the covariant derivative associated with the spatial metric $\hat{\gamma}_{jk}$.

```python
AidU_dD = ixp.zerorank2()
for i in range(DIM):
    for j in range(DIM):
        AidU_dD[i][j] += sp.diff(AidU[i], rfm.xx[j])

AidU_dDD = ixp.zerorank3()
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            AidU_dDD[i][j][k] += sp.diff(AidU[i], rfm.xx[j], rfm.xx[k])
```

## Step 3.a: Lorentz Gauge Condition & Divergence Constraint \[Back to [top](#toc)\]
$$\label{lorentz}$$

\begin{align}
\mathcal{G} &\equiv \Gamma - \partial_i A^i - \hat{\Gamma}^i_{ji} A^j &= 0, \quad &\text{Lorenz gauge condition} \\
\mathcal{C} &\equiv \partial_i E^i + \hat{\Gamma}^i_{ji} E^j  &= 0, \quad &\text{Divergence Constraint}
\end{align}

```python
# \mathcal{G} \equiv \Gamma - \partial_i A^i - \hat{\Gamma}^i_{ji} A^j
G = Gamma_ID
for i in range(DIM):
    G -= AidU_dD[i][i]
    for j in range(DIM):
        G -= rfm.GammahatUDD[i][j][i]*AidU[j]

print('G should evaluate to zero:', sp.simplify(G), '\n')

# \mathcal{C} \equiv \partial_i E^i + \hat{\Gamma}^i_{ji} E^j
C = sp.sympify(0)
for i in range(DIM):
    C += sp.diff(EidU[i], rfm.xx[i], 1)
    for j in range(DIM):
        C += rfm.GammahatUDD[i][j][i]*EidU[j]
print('C should evaluate to zero:', sp.simplify(C))
```

    G should evaluate to zero: 0

    C should evaluate to zero: 0

## Step 3.b: Check that $A^i$ satisfies the wave equation \[Back to [top](#toc)\]
$$\label{wave_eq}$$

Based on the definition of covariant derivative, we have
$$
\hat{\nabla}_{k} A^{i} = A^i_{,k} + \hat{\Gamma}^i_{mk} A^m
$$

Since $\hat{\nabla}_{k} A^{i}$ is a tensor, the covariant derivative of this will have the same indexing as a tensor $T_k^i$:

$$
\hat{\nabla}_{j} T^i_k = T^i_{k,j} + \hat{\Gamma}^i_{dj} T^d_k - \hat{\Gamma}^d_{kj} T^i_d.
$$

Therefore,
\begin{align}
\hat{\nabla}_{j} \left(\hat{\nabla}_{k} A^{i}\right) &= \left(A^i_{,k} + \hat{\Gamma}^i_{mk} A^m\right)_{,j} + \hat{\Gamma}^i_{dj} \left(A^d_{,k} + \hat{\Gamma}^d_{mk} A^m\right) - \hat{\Gamma}^d_{kj} \left(A^i_{,d} + \hat{\Gamma}^i_{md} A^m\right) \\
&= A^i_{,kj} + \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j} + \hat{\Gamma}^i_{dj}A^d_{,k} + \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m - \hat{\Gamma}^d_{kj} A^i_{,d} - \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m \\
&= {\underbrace {\textstyle A^i_{,kj}}_{\text{Term 1}}}+
{\underbrace {\textstyle \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j} + \hat{\Gamma}^i_{dj} A^d_{,k} - \hat{\Gamma}^d_{kj} A^i_{,d}}_{\text{Term 2}}} +
{\underbrace {\textstyle \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m - \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m}_{\text{Term 3}}}.
\end{align}

Thus
$$
\hat{\gamma}^{jk} \hat{\nabla}_{j} \left(\hat{\nabla}_{k} A^{i}\right) = \hat{\gamma}^{jk} \left(\text{Term 1} + \text{Term 2} + \text{Term 3}\right).
$$

We use the above to confirm

\begin{align}
\partial_t^2 A^i - \hat{\gamma}^{jk} \hat{\nabla}_j \hat{\nabla}_k A^i = 0,
\end{align}

$$
\text{Term 1} = A^i_{,kj}
$$

```python
# Term 1: A^i_{,kj}
Term1UDD = ixp.zerorank3()
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            Term1UDD[i][j][k] += AidU_dDD[i][k][j]
```

$$
\text{Term 2} = \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j} + \hat{\Gamma}^i_{dj}A^d_{,k} - \hat{\Gamma}^d_{kj} A^i_{,d}
$$

```python
# Term 2: \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j}
#        + \hat{\Gamma}^i_{dj}A^d_{,k} - \hat{\Gamma}^d_{kj} A^i_{,d}
Term2UDD = ixp.zerorank3()
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            for m in range(DIM):
                Term2UDD[i][j][k] += rfm.GammahatUDDdD[i][m][k][j]*AidU[m]    \
                                      + rfm.GammahatUDD[i][m][k]*AidU_dD[m][j] \
                                      + rfm.GammahatUDD[i][m][j]*AidU_dD[m][k] \
                                      - rfm.GammahatUDD[m][k][j]*AidU_dD[i][m]

```

$$
\text{Term 3} = \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m - \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m
$$

```python
# Term 3: \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m -
#         \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m
Term3UDD = ixp.zerorank3()
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            for m in range(DIM):
                for d in range(DIM):
                    Term3UDD[i][j][k] += ( rfm.GammahatUDD[i][d][j]*rfm.GammahatUDD[d][m][k] \
                                           -rfm.GammahatUDD[d][k][j]*rfm.GammahatUDD[i][m][d])*AidU[m]

```

$$
\hat{\gamma}^{jk} \hat{\nabla}_{j} \left(\hat{\nabla}_{k} A^{i}\right) = \hat{\gamma}^{jk} \left(\text{Term 1} + \text{Term 2} + \text{Term 3}\right),
$$

$$
\partial_t^2 A^i - \hat{\gamma}^{jk} \hat{\nabla}_j \hat{\nabla}_k A^i = 0
$$

```python
# A^i_{,kj} + \hat{\Gamma}^i_{mk,j} A^m + \hat{\Gamma}^i_{mk} A^m_{,j} +
# \hat{\Gamma}^i_{dj}A^d_{,k} + \hat{\Gamma}^i_{dj}\hat{\Gamma}^d_{mk} A^m -
# \hat{\Gamma}^d_{kj} A^i_{,d} - \hat{\Gamma}^d_{kj} \hat{\Gamma}^i_{md} A^m

Difference = ixp.zerorank1()
for i in range(DIM):
    Difference[i] = sp.diff(AidU[i], time, 2)
    for j in range(DIM):
        for k in range(DIM):
            Difference[i] += -rfm.ghatUU[k][j]*(Term1UDD[i][j][k] + Term2UDD[i][j][k] + Term3UDD[i][j][k])

for i in range(DIM):
    print(str(i)+"th component of A-field equation. Should be zero (takes a bit, please be patient): ")
    print("                "+str(sp.simplify(Difference[i])))

```

    0th component of A-field equation. Should be zero (takes a bit, please be patient):
                    0
    1th component of A-field equation. Should be zero (takes a bit, please be patient):
                    0
    2th component of A-field equation. Should be zero (takes a bit, please be patient):
                    0

# Step 4: NRPy+ Module Code Validation \[Back to [top](#toc)\]
$$\label{step4}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for the initial data we intend to use between
1. this tutorial and
2. the NRPy+ [InitialData](../edit/Maxwell/InitialData.py) module.
Since the initial data is identical between the two systems for $E^i, A^i$, and $\psi$, we also set and validate initial data for $\Gamma$.

```python
import Maxwell.InitialData as mwid
par.set_parval_from_str("Maxwell.InitialData::System_to_use","System_II")

mwid.InitialData()

# Again, to help sympy with simplifications
if radial_like_dst_xx0:
    for j in range(DIM):
        mwid.AidU[j] =  sp.refine(sp.simplify(mwid.AidU[j]), sp.Q.positive(rfm.xx[0]))
        mwid.EidU[j] =  sp.refine(sp.simplify(mwid.EidU[j]), sp.Q.positive(rfm.xx[0]))

print("Consistency check between this tutorial and NRPy+ module: ALL SHOULD BE ZERO.")

print("psi_ID - mwid.psi_ID = " + str(sp.simplify(psi_ID) - mwid.psi_ID))
print("Gamma_ID - mwid.Gamma_ID = " + str(Gamma_ID - mwid.Gamma_ID))

for i in range(DIM):
    print("AidU["+str(i)+"] - mwid.AidU["+str(i)+"] = " + str(sp.simplify(AidU[i] - mwid.AidU[i])))
    print("EidU["+str(i)+"] - mwid.EidU["+str(i)+"] = " + str(sp.simplify(EidU[i] - mwid.EidU[i])))
```

    Currently using System_II initial data
    Consistency check between this tutorial and NRPy+ module: ALL SHOULD BE ZERO.
    psi_ID - mwid.psi_ID = 0
    Gamma_ID - mwid.Gamma_ID = 0
    AidU[0] - mwid.AidU[0] = 0
    EidU[0] - mwid.EidU[0] = 0
    AidU[1] - mwid.AidU[1] = 0
    EidU[1] - mwid.EidU[1] = 0
    AidU[2] - mwid.AidU[2] = 0
    EidU[2] - mwid.EidU[2] = 0

# Step 5: 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-VacuumMaxwell_InitialData.pdf](Tutorial-VacuumMaxwell_InitialData.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-VacuumMaxwell_InitialData")
```

    Created Tutorial-VacuumMaxwell_InitialData.tex, and compiled LaTeX file to
        PDF file Tutorial-VacuumMaxwell_InitialData.pdf


# Further information

(difference_between_a_list_and_a_tuple)=

## What is the difference between a Python list and a Python tuple?

Two of the most used Python iterables are lists and tuples. In practice they
have a number of similarities, they are both ordered collections of objects that
can be used in list comprehensions as well as in other ways.

- Tuples are **immutable**
- Lists are **mutable**

This means that once created tuples cannot be changed and lists can.

As a general rule of thumb: if you do not need to modify your iterable then use
a tuple as they are more computationally efficient.

This blog post is a good explanation of the difference:
<https://www.afternerd.com/blog/difference-between-list-tuple/>

## Why does the sum of booleans counts the `True`s?

In the tutorial and elsewhere we created a list of booleans and then take the
sum. Here are some of the steps:

```python
samples = ("Red", "Red", "Blue")
```

```python
booleans = [sample == "Red" for sample in samples]
booleans
```

    [True, True, False]

When we take the `sum` of that list we get a numeric value:

```python
sum(booleans)
```

    2

This has in fact counted the `True` values as 1 and the `False` values as 0.

```python
int(True)
```

    1

```python
int(False)
```

    0

## What is the difference between `print` and `return`?

In functions you see we use the `return` statement. This does two things:

1. Assigns a value to the function run;
2. Ends the function.

The `print` statement **only** displays the output.

As an example let us create the following set:

$$
    S = \{f(x)\text{ for }x \in \{0, \pi / 4, \pi / 2, 3\pi / 4\}\}
$$

where $f(x)= \cos^2(x)$.

The correct way to do this is:

```python
import sympy as sym

def f(x):
    """
    Return the square of the cosine of x
    """
    return sym.cos(x) ** 2

S = [f(x) for x in (0, sym.pi / 4, sym.pi / 2, 3 * sym.pi / 4)]
S
```

    [1, 1/2, 0, 1/2]

If we replaced the `return` statement in the function definition with a `print` we obtain:

```python
def f(x):
    """
    Return the square of the cosine of x
    """
    print(sym.cos(x) ** 2)

S = [f(x) for x in (0, sym.pi / 4, sym.pi / 2, 3 * sym.pi / 4)]
```

    1
    1/2
    0
    1/2

We see now that as the function has been run it displays the output.

**However** if we look at what `S` is we see that the function has not returned
anything:

```python
S
```

    [None, None, None, None]

Here are some other materials on this subject:

- <https://www.tutorialspoint.com/Why-would-you-use-the-return-statement-in-Python>
- <https://pythonprinciples.com/blog/print-vs-return/>

## How does Python sample randomness?

When using the Python random module we are in fact generating a pseudo random
process. True randomness is actually not common.

Pseudo randomness is an important area of mathematics as strong algorithms that
create unpredictable sequences of numbers are vital to cryptographic security.

The specific algorithm using in Python for randomness is called the Mersenne
twister algorithm is state of the art.

You can read more about this here:
<https://docs.python.org/3/library/random.html#module-random>.

## What is the difference between a docstring and a comment

In Python it is possible to write statements that are ignored using the `#`
symbol. This creates something called a "comment". For example:

```python
# create a list to represent the tokens in a bag
bag = ["Red", "Red", "Blue"]
```

A docstring however is something that is "attached" to a function and can be
accessed by Python.

If we rewrite the function to sample the experiment of the tutorial without a
docstring but using comments we will have:

```python
def sample_experiment(bag):
    # Select a token
    selected_token = pick_a_token(container=bag)

    # If the token is red then the probability of selecting heads is 2/3
    if selected_token == "Red":
        probability_of_selecting_heads = 2 / 3
    # Otherwise it is 1 / 2
    else:
        probability_of_selecting_heads = 1 / 2

    # Select a coin according to the probability.
    if random.random() < probability_of_selecting_heads:
        coin = "Heads"
    else:
        coin = "Tails"

    # Return both the selected token and the coin.
    return selected_token, coin
```

Now if we try to access the help for the function we will not get it:

```python
help(sample_experiment)
```

    Help on function sample_experiment in module __main__:

    sample_experiment(bag)

Furthermore, if you look at the code with comments you will see that because of
the choice of variable names the comments are in fact redundant.

In software engineering it is generally accepted that comments indicate that
your code is not clear and so it is preferable to write clear documentation
explaining why something is done through docstrings.

```python
def sample_experiment(bag):
    """
    This samples a token from a given bag and then
    selects a coin with a given probability.

    If the sampled token is red then the probability
    of selecting heads is 2/3 otherwise it is 1/2.

    This function returns both the selected token
    and the coin face.
    """
    selected_token = pick_a_token(container=bag)

    if selected_token == "Red":
        probability_of_selecting_heads = 2 / 3
    else:
        probability_of_selecting_heads = 1 / 2

    if random.random() < probability_of_selecting_heads:
        coin = "Heads"
    else:
        coin = "Tails"
    return selected_token, coin
```

Here are some resources on this:

- <https://blog.codinghorror.com/coding-without-comments/>
- <https://visualstudiomagazine.com/articles/2013/07/26/why-commenting-code-is-still-bad.aspx>


# Lecture 4: Ordinary differential equation extensions

This notebook presents some extensions of material handled in lectures. It presents

- A general method for non-homogeneous second-order ordinary differential equations; and
- The formulation of the differential equation for an elastic rod in one dimension via an integral approach.

We present an extension to what was covered in the lecture, namely the *variation of parameters method* for non-homogeneous equations.

# Method of undetermined coefficients

In the previous lecture, we covered the solution of non-homogeneous second-order differential equations. Although not explicitly stated, the method we used is known as *method of undetermined coefficients*. It relied on computing a solution to the homogeneous problem, and then having a suitable 'guess' for for the particular solution $y_{P}$.

But what if we do not know what $y_{P}$ should look like? In this case we can use the *variation of parameters* method.

# Variation of parameters method

We wish to find a solution to the problem

$$
\frac{d^{2}y}{dx^{2}} + p(x) \frac{dy}{dx} + q(x) y = f(x)
$$

By linearity, if $y_{P}$ is a solution to the above equation and $y_{CF}$ is a solution to the homogeneous equation,

$$
\frac{d^{2}y}{dx^{2}} + p(x) \frac{dy}{dx} + q(x) y = 0
$$

then the general solution is $y = y_{CF} + y_{P}$.

For the case of constant coefficients, we know how to find the solution to the homogeneous problem, which is most cases is of the form

$$
y_{CF} = \underbrace{A_{1} e^{\lambda_{1} x}}_{y_{1}} + \underbrace{A_{2} e^{\lambda_{2} x}}_{y_{2}}
$$

where $\lambda$ are the roots of $\lambda + p\lambda + q$.

Consider a particular solution $y_{P}$ of the form

$$
y_{P} = v_{1}(x) y_{1} + v_{2}(x)y_{2}
$$

with the objective of finding the functions $v_{1}$ and $v_{2}$. We will insist of the constraint

$$
\frac{d v_{1}}{dx} y_{1} + \frac{d v_{2}}{dx} y_{2} = 0.
$$

Computing derivatives of $y_{P}$:

$$
\begin{align}
\frac{d y_{P}}{d x} &= v_{1} \frac{d y_{1}}{dx} + v_{2} \frac{d y_{2}}{dx}
\\
\frac{d^{2} y_{P}}{d x^{2}} &= \frac{d v_{1}}{dx} \frac{d y_{1}}{dx} + \frac{d v_{2}}{dx} \frac{d y_{2}}{dx} +
v_{1} \frac{d^{2} y_{1}}{dx^{2}} + v_{2} \frac{d^{2} y_{2}}{dx^{2}}
\end{align}
$$

Inserting the expressions for $y_{P}$ is its derivatives into the original equations (keeping in mind that $y_{1}$ and $y_{2}$ are solutions to the homogeneous problem), we find

$$
\frac{d v_{1}}{dx} \frac{d y_{1}}{dx} + \frac{d v_{2}}{dx} \frac{d y_{2}}{dx} = f(x)
$$

Combining the above equations with the conditon $v_{1}^{\prime} y_{1} + v_{2}^{\prime} y_{2} = 0$,

$$
\begin{align}
\frac{d v_{1}}{dx} &= -\frac{y_{2} f(x)}{W(x)}
\\
\frac{d v_{2}}{dx} &= \frac{y_{1} f(x)}{W(x)}
\end{align}
$$

where $W(x) = y_{1} y^{\prime}_{2} - y^{\prime}_{1} y_{2}$ is known as the *Wronskian*. Integrating both sides of the above equations,

$$
\begin{align}
v_{1} &= -\int \frac{y_{2} f(x)}{W(x)} dx
\\
v_{2} &= \int \frac{y_{1} f(x)}{W(x)} dx
\end{align}
$$

## Solving a given non-homogeneous equation symbolically

We now solve and ODE symbolically with a RHS side $f(x)$ of unknown form. We first import SymPy:

```python
from sympy import *
init_printing()
from IPython.display import display
```

We first solve the homogeneous equation:

```python
x = Symbol("x")
y = Function("y")
f = Function("f")
eqn = Eq(Derivative(y(x), x, x) + 2*Derivative(y(x), x) + y(x), 0)
display(eqn)
dsolve(eqn)
```

Now solve the non-homogeneous case for some $f(x)$:

```python
eqn = Eq(Derivative(y(x), x, x) + 2*Derivative(y(x), x) + y(x), f(x))
dsolve(eqn)
```

Note the integrand involving the product of $f(x)$ and terms from solution to the homogeneous problem, as expected from the variation of parameters method.

# From balance laws to differential equations

It is common in science and engineering to derive differential equations from integral balance laws. The example of an elastic one-dimensional rod was covered in Lecture 4 (but it not in the printed lecture notes). That example repeated and expanded here.

Before presenting the example, we consider a theorem that we will use in the derivation.

## Localisation theorem

For a continuous function $\phi(x)$ on $\Omega$ statisfies

$$
\int_{B} \phi \, dx = 0 \quad \forall B \subset \Omega
$$

then

$$
\phi(x) = 0 \quad \forall x \in \Omega.
$$

*Proof:*
The proof is by contradiction. Assume at some point $x_{0}$ that $\phi(x_{0}) = \delta > 0$. Since $\phi$ is continuous, there is a neighborhood $D \in \Omega$ around $x_{0}$ on which $\phi > 0$. On $D$, $\int_{D} \phi \, dx > 0$, which contradicts the first part of the theorem.

Note that the notation '$\forall B \subset \Omega$' means all subdomains of $\Omega$.

## An elastic rod in one dimension

An elastic rod in one dimension can transmit forces along its axis only (there is not shear or bending - it is not a beam). The normal force in the rod is $N$, and by definition $h = N n_{x}$, where $h$ is a force and $n_{x}$ is the outward unit normal vector. We assume the the normal force in the rod is smooth, i.e. it is continuous as its first derivative.

Consider the below bar with forces acting at the ends and a distributed load $f$ (force per unit length) acting along the rod.

Summing the forces acting on the rod,

$$
\int_{0}^{L} f \, dx + h_{L} + h_{0} = 0.
$$

Replacing $h$ by $N n_{x}$ (in one dimension $n_{x} = \pm 1$),

$$
\int_{0}^{L} f \, dx + N_{L} - N_{0} = 0.
$$

We can convert the $N_{L} - N_{0}$ term to an integral from $0$ to $L$:

$$
\int_{0}^{L} f \, dx + \int_{0}^{L} \frac{dN}{dx} \, dx = 0.
$$

Since $f$ and $dN$/dx are continuous, from the localisation theorem implies that

$$
\frac{dN}{dx} + f = 0.
$$

This is the equilibrium equation. For an elastic bar, the normal force in terms of deformation is given by Hooke's law:

$$
N = EA\frac{du}{dx},
$$

where $E$ is Young's modulus, $A(x)$ is the cross-sectional area of the rod, $u$ is the displacement and $du/dx$ is the strain.  Inserting the expression for $N$ into the equilibrium equation:

$$
\frac{d}{dx}\left(EA \frac{du}{dx}\right) + f = 0.
$$

This is a second-order differential equation, with $u$ being the dependent variable.