# Manufactured solution for BP3-like problem

The manufactured solution is going to exactly solve the following problem:

$$
\begin{aligned}
    -\frac{\partial\sigma_{ij}(\boldsymbol{u})}{\partial x_i} &= 0 & \text{ in } & \Omega \\
    \sigma_{ij} &= c_{ijkl} \frac{\partial u_k}{\partial x_l} & \text{ in } & \Omega \\
    u_i &= u_i^* & \text{ on } & \Gamma_D \\
    u_1^- - u_1^+ &= 0 & \text{ on } & \Gamma_F \\
    u_2^- - u_2^+ &= -S & \text{ on } & \Gamma_F \\
    -(\tau^0 + \tau) &= (\sigma_n^0-\sigma_n)f(V,\psi) + \eta V & \text{ on } & \Gamma_F \\
    \frac{d\psi}{dt} &= g(V,\psi) + s(\vec x, t) & \text{ on } & \Gamma_F \\
    \frac{dS}{dt} &= V & \text{ on } & \Gamma_F
\end{aligned}
$$
where
$$
\begin{aligned}
    f(V, \psi) &= a \cdot \mathrm{asinh}\left(\frac{V}{2V_0} \exp\left(\frac{\psi}{a}\right)\right) \\
    g(V, \psi) &= \frac{b V_0}{L} \left(\exp\left(\frac{f_0-\psi}{b}\right) - \frac{|V|}{V_0}\right) \\
    s(\vec{x}, t) &= - g(V^*, \psi^*) + \frac{d\psi^*}{dt} \\
    \tau &= -\sigma_{12} \\
    \sigma_n &= \sigma_{11}
\end{aligned}
$$

```python
from sympy import *
init_printing()
```

We first define symbols and general functions, which are going to be specified later.

```python
x, y, t = symbols('x y t')
te, tw = symbols('t_e t_w')
V0, Vp = symbols('V_0 V_p')
a, sn, eta = symbols('a sigma_n eta')
K = Function('K')(t)
V_star = Function('V^*')(y, t)
tau_star = Function('tau^*')(y, t)
tau_pre = Function('tau^0')(y)
sn_star = symbols('sigma_n^*')
sn_pre = Function('sigma_n^0')(y)
E, nu = symbols('E nu', real=True, positive=True)
lamda = E*nu/((1-2*nu)*(1+nu))
mu = E/(2*(1+nu))
```

$\psi^*$ is the state variable.

```python
psi_star = a * log((2*V0/V_star) * sinh((tau_star + tau_pre + eta*V_star)/(a*(sn_star - sn_pre))))
psi_star
```

We need the time derivative $\frac{d\psi^*}{dt}$

```python
psi_star.diff(t).simplify()
```

$u_i^*$ is solution for the displacement.

```python
k = symbols('k', real=True, positive=True)
f = K*x*exp(-k*x)
phi = cos(k*y)*f
phi
```

```python
sxx = phi.diff(y, 2)
syy = phi.diff(x, 2)
sxy = -phi.diff(x).diff(y)
exx = (sxx - nu/(1-nu)*syy)*(1-nu**2)/E
eyy = (syy - nu/(1-nu)*sxx)*(1-nu**2)/E
exy = sxy*(1+nu)/E
ux_star = integrate(exx, x)
uy_star = integrate(eyy, y)
ux_star.simplify()
```

```python
uy_star.simplify()
```

```python
simplify(ux_star.diff(y) + uy_star.diff(x) - 2*exy)
```

Let's introduce slip:

```python
uxm_star = ux_star.subs(k, -k)
uym_star = uy_star.subs(k, -k)
uxp_star = ux_star
uyp_star = uy_star
```

$S^*$ is the on-fault displacement.

```python
S_star_fun = -(uym_star - uyp_star).subs(x, 0).simplify()
S_star_fun
```

$\tau^*$ is the on fault shear traction.

```python
s21 = mu * (uxm_star.diff(y) + uym_star.diff(x))
tau_star_fun = -s21.subs(x, 0).simplify()
tau_star_fun
```

Check continuity:

```python
s21 = mu * (uxp_star.diff(y) + uyp_star.diff(x))
tau_star_fun = -s21.subs(x, 0).simplify()
tau_star_fun
```

$\sigma_n^*$ is the on fault normal stress.

```python
s11 = (lamda + 2*mu) * uxm_star.diff(x) + lamda * uym_star.diff(y)
sn_star_fun = s11.subs(x, 0).simplify()
sn_star_fun
```

Check continuity:

```python
s11 = (lamda + 2*mu) * uxp_star.diff(x) + lamda * uyp_star.diff(y)
sn_star_fun = s11.subs(x, 0).simplify()
sn_star_fun
```

Slip-rate $V^*$

```python
V_star_fun = S_star_fun.diff(t)
V_star_fun
```

Acceleration $\frac{dV^*}{dt}$

```python
V_star_fun.diff(t)
```

The following lines define the functions $K$ and necessary time derivatives.

```python
K_fun = 2.0 * (atan((t-te)/tw) + pi/2) / pi
K_fun
```

```python
K_fun.subs(t, 0)
```

```python
K_fun.subs(t, te)
```

```python
K_fun.diff(t)
```

```python
K_fun.diff(t).diff(t).simplify()
```

## Plot solution

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

```python
y1 = -0.2
y2 = -0.5
y3 = -0.8
mu_val = 3.464**2 * 2.670
v = dict()
v[eta] = 3.464 * 2.670
v[nu] = 0.25
v[E] = 2*mu_val*(1+v[nu])
v[te] = 50
v[tw] = 1
v[k] = float(pi.evalf())
v[V0] = 1.0e-6
v[a] = 0.015
v[sn_pre] = 25 * abs(sin(v[k] * y))
v[tau_pre] = 20 * sin(v[k] * y)
v
```

```python
v[K] = K_fun.subs(v)
v[K]
```

```python
V_star_val = V_star_fun.subs(v).simplify()
def V_star_eval():
    def F(Y, T):
        return float(V_star_val.subs(y, Y).subs(t, T).evalf())
    return np.vectorize(F)
V_star_val
S_star_val = S_star_fun.subs(v).simplify()
def S_star_eval():
    def F(Y, T):
        return float(S_star_val.subs(y, Y).subs(t, T).evalf())
    return np.vectorize(F)
S_star_val
```

```python
T1 = np.linspace(0, 2*v[te], 400)
plt.plot(T1, V_star_eval()(y1, T1))
plt.plot(T1, V_star_eval()(y2, T1))
```

```python
T2 = np.linspace(v[te] - 10*v[tw], v[te] + 10*v[tw], 400)
plt.plot(T2, V_star_eval()(y1, T2))
plt.plot(T2, V_star_eval()(y2, T2))
```

```python
def tau_pre_eval():
    def F(Y):
        return float(v[tau_pre].subs(y, Y).evalf())
    return np.vectorize(F)
tau_star_val = tau_star_fun.subs(v).simplify()
def tau_star_eval():
    def F(Y, T):
        return float(tau_star_val.subs(y, Y).subs(t, T).evalf())
    return np.vectorize(F)
tau_star_val
```

```python
sn_star_val = sn_star_fun.subs(v)
def sn_star_eval():
    def F(Y):
        return float(sn_star_val.subs(y, Y).evalf())
    return np.vectorize(F)
sn_star_val
```

```python
plt.plot(T1, tau_pre_eval()(y1) + tau_star_eval()(y1, T1))
plt.plot(T1, tau_pre_eval()(y2) + tau_star_eval()(y2, T1))
```

```python
plt.plot(T2, tau_pre_eval()(y1) + tau_star_eval()(y1, T2))
plt.plot(T2, tau_pre_eval()(y2) + tau_star_eval()(y2, T2))
```

```python
psi_star_val = psi_star.subs(v).subs(tau_star, tau_star_val).subs(sn_star, sn_star_val).subs(V_star, V_star_val)
def psi_star_eval():
    def F(Y, T):
        return float(psi_star_val.subs(y, Y).subs(t, T).evalf())
    return np.vectorize(F)
def dpsidt_star_eval():
    def F(Y, T):
        return float(psi_star_val.diff(t).subs(y, Y).subs(t, T).evalf())
    return np.vectorize(F)
```

```python
[psi_star_eval()(y2, 0), psi_star_eval()(y2, v[te]), psi_star_eval()(y2, 2*v[te])]
```

    [array(0.7612255), array(0.50877239), array(0.51309766)]

```python
plt.plot(T1, psi_star_eval()(y1, T1))
plt.plot(T1, psi_star_eval()(y2, T1))
```

```python
plt.plot(T2, psi_star_eval()(y1, T2))
plt.plot(T2, psi_star_eval()(y2, T2))
```

## Test time integration

```python
from scipy.integrate import solve_ivp
from scipy.optimize import toms748
```

```python
y_eval = y2
tau_pre1 = tau_pre_eval()(y_eval)
tau_star1 = lambda t: tau_star_eval()(y_eval, t)
sn_pre1 = float(v[sn_pre].subs(y, y_eval))
psi_star1 = lambda t: psi_star_eval()(y_eval, t)
dpsidt_star1 = lambda t: dpsidt_star_eval()(y_eval, t)
S_star1 = lambda t: S_star_eval()(y_eval, t)
V_star1 = lambda t: V_star_eval()(y_eval, t)
(tau_pre1, tau_star1(0), sn_pre1, psi_star1(0), dpsidt_star1(0), V_star1(0))
```

    (array(-20.),
     array(0.03999467),
     25.0,
     array(0.7612255),
     array(-0.00063192),
     array(1.1917651e-05))

```python
def friction_law(V, sn, psi):
    a = 0.015
    e = np.exp(psi / a)
    f = a * np.arcsinh((V / (2.0 * 1.0e-6)) * e)
    return sn * f

def slip_rate(tau, sn, psi):
    eta = 3.464*2.670/2
    L = 0
    R = -tau / eta
    if L == R:
        return 0.0
    if L > R:
        L, R = R, L
    return toms748(
        lambda V: tau + friction_law(V, sn, psi) + eta * V,
        L,
        R,
        xtol=1e-100)

def state_law(V, psi):
    return 0.02 * 1.0e-6 / 0.2 * (np.exp((0.6 - psi) / 0.02) - np.abs(V) / 1.0e-6)

def y0():
    y0 = np.zeros((2,))
    y0[0] = psi_star1(0)
    y0[1] = S_star1(0)
    return y0

def F(t, y):
    fy = np.ndarray(y.shape)
    psi = y[0]
    V = slip_rate(tau_pre1 + tau_star1(t), sn_pre1, psi)
    fy[0] = state_law(V, psi) - state_law(V_star1(t), psi_star1(t)) + dpsidt_star1(t)
    fy[1] = V
    return fy
```

```python
t0 = 0
tend = 2*v[te]
result = solve_ivp(F, (t0, tend), y0(), method='RK45', rtol=1e-7, atol=1e-50)
result
```

      message: 'The solver successfully reached the end of the integration interval.'
         nfev: 530
         njev: 0
          nlu: 0
          sol: None
       status: 0
      success: True
            t: array([0.00000000e+00, 3.71392064e-02, 4.08531270e-01, 4.12245191e+00,
           1.04246702e+01, 1.59221740e+01, 2.05707303e+01, 2.45411863e+01,
           2.79289363e+01, 3.08256539e+01, 3.33062639e+01, 3.54345051e+01,
           3.72640363e+01, 3.88401405e+01, 4.02010162e+01, 4.13789027e+01,
           4.24010279e+01, 4.32904100e+01, 4.40665356e+01, 4.47459313e+01,
           4.53426478e+01, 4.58686706e+01, 4.63342724e+01, 4.67483203e+01,
           4.71185577e+01, 4.74887951e+01, 4.77880639e+01, 4.80873327e+01,
           4.83637577e+01, 4.86240455e+01, 4.88905434e+01, 4.90651693e+01,
           4.92218202e+01, 4.93784711e+01, 4.95203744e+01, 4.96545665e+01,
           4.97894161e+01, 4.99392409e+01, 5.00831349e+01, 5.02270289e+01,
           5.03685195e+01, 5.05214042e+01, 5.07036396e+01, 5.09460547e+01,
           5.11872092e+01, 5.14372177e+01, 5.17152890e+01, 5.20339679e+01,
           5.24066998e+01, 5.28500507e+01, 5.33854796e+01, 5.40414976e+01,
           5.48566325e+01, 5.58836841e+01, 5.71959655e+01, 5.88966020e+01,
           6.11324903e+01, 6.41149279e+01, 6.81478108e+01, 7.36572326e+01,
           8.12004938e+01, 9.14444760e+01, 1.00000000e+02])
     t_events: None
            y: array([[7.61225496e-01, 7.61202017e-01, 7.60966221e-01, 7.58500669e-01,
            7.53790685e-01, 7.48978933e-01, 7.44209939e-01, 7.39440266e-01,
            7.34676987e-01, 7.29912519e-01, 7.25141739e-01, 7.20357527e-01,
            7.15551786e-01, 7.10715117e-01, 7.05836789e-01, 7.00904619e-01,
            6.95904817e-01, 6.90821781e-01, 6.85637795e-01, 6.80332609e-01,
            6.74882837e-01, 6.69261068e-01, 6.63434532e-01, 6.57363019e-01,
            6.50995441e-01, 6.43447765e-01, 6.36202890e-01, 6.27597091e-01,
            6.18053869e-01, 6.07227186e-01, 5.93744534e-01, 5.83302372e-01,
            5.72711662e-01, 5.60934957e-01, 5.49335467e-01, 5.37766579e-01,
            5.25899428e-01, 5.12982083e-01, 5.01428027e-01, 4.91174274e-01,
            4.82589401e-01, 4.74995372e-01, 4.67988802e-01, 4.61415623e-01,
            4.57126421e-01, 4.54286114e-01, 4.52404000e-01, 4.51317646e-01,
            4.50949354e-01, 4.51258426e-01, 4.52224503e-01, 4.53839640e-01,
            4.56103718e-01, 4.59021396e-01, 4.62599922e-01, 4.66847509e-01,
            4.71771956e-01, 4.77378459e-01, 4.83663303e-01, 4.90596359e-01,
            4.98088863e-01, 5.05980331e-01, 5.11285090e-01],
           [5.96041438e-04, 5.96484444e-04, 6.00950990e-04, 6.49592702e-04,
            7.53016552e-04, 8.74458404e-04, 1.01252718e-03, 1.17034583e-03,
            1.34984516e-03, 1.55355617e-03, 1.78408588e-03, 2.04428900e-03,
            2.33724452e-03, 2.66629399e-03, 3.03507317e-03, 3.44755868e-03,
            3.90812919e-03, 4.42164792e-03, 4.99357518e-03, 5.63012362e-03,
            6.33847666e-03, 7.12710203e-03, 8.00621514e-03, 8.98848949e-03,
            1.00902009e-02, 1.14892740e-02, 1.29255581e-02, 1.47484228e-02,
            1.69163645e-02, 1.95618838e-02, 2.31364018e-02, 2.61249159e-02,
            2.93618310e-02, 3.32204579e-02, 3.73108318e-02, 4.17085730e-02,
            4.65948230e-02, 5.24199679e-02, 5.81709449e-02, 6.38046249e-02,
            6.90020222e-02, 7.40667963e-02, 7.92612531e-02, 8.48184101e-02,
            8.90745568e-02, 9.24755365e-02, 9.53828545e-02, 9.79189406e-02,
            1.00150406e-01, 1.02121271e-01, 1.03863443e-01, 1.05401324e-01,
            1.06754338e-01, 1.07938561e-01, 1.08967881e-01, 1.09854863e-01,
            1.10611346e-01, 1.11248762e-01, 1.11778142e-01, 1.12209868e-01,
            1.12553712e-01, 1.12820205e-01, 1.12959050e-01]])
     y_events: None

```python
np.min(np.diff(result['t']))
```

```python
plt.plot(result['t'], result['y'][0])
```

```python
plt.plot(result['t'], np.abs(result['y'][0] - psi_star1(result['t'])))
```

```python
plt.plot(result['t'], result['y'][1])
```

```python

```


# 物理系を表現する

第三回の実習では、量子コンピュータの並列性を利用した計算の代表例として、量子系のダイナミクスシミュレーションについて学びます。

```{contents} 目次
---
local: true
---
```

$\newcommand{\bra}[1]{\langle #1 |}$
$\newcommand{\ket}[1]{| #1 \rangle}$
$\newcommand{\upket}{\ket{\!\uparrow}}$
$\newcommand{\downket}{\ket{\!\downarrow}}$
$\newcommand{\rightket}{\ket{\!\rightarrow}}$
$\newcommand{\leftket}{\ket{\!\leftarrow}}$

## 量子系のダイナミクスとは

量子力学について少しでも聞いたことのある方は、量子力学の根幹にシュレーディンガー方程式というものが存在することを知っているかと思います。この方程式は

$$
i \hbar \frac{\partial}{\partial t} \ket{\psi (t)} = H \ket{\psi (t)}
$$

などと表現され、時刻$t$のある系の状態$\ket{\psi (t)}$の時間微分（左辺）が$\ket{\psi (t)}$へのハミルトニアンという演算子の作用で定まる（右辺）ということを表しています。ただしこの「微分形」の方程式は我々の目的には少し使いづらいので、ここでは等価な「積分形」にして

$$
\ket{\psi (t_1)} = \exp \left( -\frac{i}{\hbar} \int_{t_0}^{t_1} H dt \right) \ket{\psi (t_0)}
$$

と書いておきます。特に、ハミルトニアン$H$が直接時間に依存しない場合は

$$
\ket{\psi (t_1)} = \exp \left( -\frac{i}{\hbar} (t_1 - t_0) H \right) \ket{\psi (t_0)}
$$

です。以下このケースのみを考えます。量子状態に対する演算子（線形演算子）の指数関数もまた演算子なので、積分形のシュレーディンガー方程式は「$e^{-i/\hbar (t_1-t_0) H}$という演算子が系を時刻$t_0$の初期状態$\ket{\psi(t_0)}$から時刻$t_1$の状態$\ket{\psi(t_1)}$に発展させる」と読めます。さらに、定義上ハミルトニアンは「エルミート演算子」であり、それに虚数単位をかけて指数の冪にした$e^{-i/\hbar t H}$（以下これを時間発展演算子$U_H(t)$と呼びます）は「ユニタリ演算子」です（このあたりの線形代数の用語にあまり馴染みがなくても、そういうものかと思ってもらえれば結構です）。

ユニタリ演算子は量子計算の言葉で言えばゲートにあたります。したがって、ある量子系に関して、その初期状態を量子レジスタで表現でき、時間発展演算子を量子コンピュータの基本ゲートの組み合わせで実装できれば、その系のダイナミクス（＝時間発展）シミュレーションを量子コンピュータで行うことができます。

### 例：核磁気の歳差運動

シミュレーションの詳しい話をする前に、これまで量子力学と疎遠だった方のために、ハミルトニアンや時間発展とは具体的にどういうことか、簡単な例を使って説明します。

空間中に固定されたスピン$\frac{1}{2}$原子核一つを考えます。ある方向（Z方向とします）のスピン$\pm \frac{1}{2}$の状態をそれぞれ$\upket, \downket$で表します。量子力学に馴染みのない方のための説明例で大いに量子力学的な概念を使っていますが、何の話かわからなければ「2つの基底ケットで表現される、量子ビットのような物理系がある」と考えてください。量子ビットのような物理系なので、系の状態は一般に$\upket$と$\downket$の重ね合わせになります。

時刻$t_0$で系が$\ket{\psi(t_0)} = \upket$にあるとします。時刻$t_1$での系の状態を求めることは

$$
\ket{\psi (t_1)} = \alpha (t_1) \upket + \beta (t_1) \downket
$$

の$\alpha (t_1)$と$\beta (t_1)$を求めることに相当します。ここで$\alpha (t_0) = 1, \beta (t_0) = 0$です。

この原子核に$X$方向の一定磁場をかけます。非ゼロのスピンを持つ粒子はスピンベクトル$\vec{\sigma}$と平行な磁気モーメント$\vec{\mu}$を持ち、磁場$\vec{B}$のもとでエネルギー$-\vec{B}\cdot\vec{\mu}$を得ます。ハミルトニアンとは実は系のエネルギーを表す演算子なので、この一定磁場だけに注目した場合の系のハミルトニアンは、何かしらの定数$\omega$とスピンベクトルの$X$成分$\sigma^X$を用いて$H = \hbar \omega \sigma^X$と書けます。

量子力学では$\sigma^X$は演算子であり、$\upket$と$\downket$に対して

$$
\sigma^X \upket = \downket \\
\sigma^X \downket = \upket
$$

と作用します。時間発展演算子$U_H(t)$は

$$
U_H(t) = \exp (-i \omega t \sigma^X) = \sum_{n=0}^{\infty} \frac{1}{n!} (-i \omega t)^n (\sigma^X)^n = I + (-i \omega t) \sigma^X + \frac{1}{2} (-i \omega t)^2 (\sigma^X)^2 + \frac{1}{6} (-i \omega t)^3 (\sigma^X)^3 \cdots
$$

ですが（$I$は恒等演算子）、上の$\sigma^X$の定義からわかるように$(\sigma^X)^2 = I$なので

```{math}
:label: exp_sigmax
\begin{align}
\exp (-i \omega t \sigma^X) & = \left[ 1 + \frac{1}{2} (-i \omega t)^2 + \cdots \right] I + \left[(-i \omega t) + \frac{1}{6} (-i \omega t)^3 + \cdots \right] \sigma^X \\
& = \cos(\omega t) I - i \sin(\omega t) \sigma^X
\end{align}
```

と書けます。したがって、

```{math}
:label: spin_exact
\ket{\psi (t_1)} = U_H(t_1 - t_0) \ket{\psi (t_0)} = \exp [-i \omega (t_1 - t_0) \sigma^X] \upket = \cos[\omega (t_1 - t_0)] \upket - i \sin[\omega (t_1 - t_0)] \downket
```

です。任意の時刻$t_1$のスピンの状態が基底$\upket$と$\downket$の重ね合わせとして表現されました。

このように、系のエネルギーの表式からハミルトニアンが決まり、その指数関数を初期状態に作用させることで時間発展後の系の状態が求まります。

ちなみに、$\ket{\psi (t_1)}$は$t_1 = t_0$で$\upket$、$t_1 = t_0 + \pi / (2 \omega)$で$(-i)\downket$となり、以降$\upket$と$\downket$を周期的に繰り返します。実は、その間の状態はスピンが$Y$-$Z$平面内を向いている状態に相当します。スピンが0でない原子核に磁場をかけると、スピンと磁場の方向が揃っていなければ磁場の方向を軸にスピンが歳差運動（すりこぎ運動）をします。これはコマが重力中で起こす運動と同じで、核磁気共鳴（NMR、さらに医学応用のMRI）の原理に深く関わります。

### 量子コンピュータ上での表現

すでに触れましたが、上の例で核のスピンは量子ビットのように2つの基底ケットを持ちます（2次元量子系です）。さらに、お気づきの方も多いと思いますが、$\sigma^X$の$\upket$と$\downket$への作用は$X$ゲートの$\ket{0}$と$\ket{1}$への作用そのものです。このことから、核磁気の歳差運動が極めて自然に量子コンピュータでシミュレートできることがわかるかと思います。

実際には、時間発展演算子は$\sigma^X$そのものではなくその指数関数なので、量子コンピュータでも$\exp (-i \theta/2 X)$に対応する$R_{x} (\theta)$ゲートを利用します。これまで紹介されませんでしたが、$R_{x}$ゲートはパラメータ$\theta$をとり、

$$
R_{x}(\theta)\ket{0} = \cos\frac{\theta}{2}\ket{0} - i\sin\frac{\theta}{2}\ket{1} \\
R_{x}(\theta)\ket{1} = -i\sin\frac{\theta}{2}\ket{0} + \cos\frac{\theta}{2}\ket{1}
$$

という変換を行います。上の核スピン系を量子コンピュータでシミュレートするには、1量子ビットで$R_{x} (2 \omega (t_1 - t_0)) \ket{0}$を計算する以下の回路を書けばいいだけです。

```python
import numpy as np
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit import Parameter
```

```python
circuit = QuantumCircuit(QuantumRegister(1, 'q'), ClassicalRegister(1, 'c'))
circuit.rx(Parameter(r'$2 \omega (t_1 - t_0)$'), 0)
circuit.measure(0, 0)
circuit.draw('mpl', initial_state=True)
```

### ハミルトニアンの対角化

再び量子コンピュータを離れて、量子・古典に関わらずデジタル計算機で量子ダイナミクスのシミュレーションをする際の一般論をします。

上の核スピンの例ではハミルトニアンが単純だったので、式{eq}`spin_exact`のように厳密解が求まりました。特に、導出において$(\sigma^X)^2 = I$という恒等式が非常に重要でした。しかし、一般のハミルトニアンでは、何乗しても恒等演算子の定数倍にたどり着く保証がありません。

累乗して恒等演算子にならないようなハミルトニアンであっても、系の次元が小さい場合は「対角化」という作業で厳密解を得られます。ハミルトニアンの対角化とは、ハミルトニアンの作用が実数をかけることと等しくなるようなケットを探してくること、つまり

$$
H\ket{\phi_j} = \hbar \omega_j \ket{\phi_j}, \quad \omega_j \in \mathbb{R}
$$

が成り立つような$\ket{\phi_j}$を見つけることを指します。このような$\ket{\phi_j}$を「固有値$\hbar \omega_j$を持つ$H$の固有ベクトル」と呼びます。「エネルギー固有状態」と呼ぶこともあります。系の次元が$N$であれば、独立な固有ベクトルが$N$個存在します。

例えば上の例では$H = \hbar \omega \sigma^X$ですが、

```{math}
:label: left_right_kets
\rightket := \frac{1}{\sqrt{2}}(\upket + \downket) \\
\leftket := \frac{1}{\sqrt{2}}(\upket - \downket)
```

という2つの状態を考えると

$$
\sigma^X \rightket = \rightket \\
\sigma^X \leftket = -\leftket
$$

なので、これらが固有値$\pm \hbar \omega$の$H$の固有ベクトルとなっていることがわかります。

固有値$\hbar \omega_j$のハミルトニアン$H$の固有ベクトル$\ket{\phi_j}$は自動的に時間発展演算子$U_H(t)$の固有値$e^{-i\omega_j t}$の固有ベクトルでもあります。

$$
U_H(t) \ket{\phi_j} = \exp \left( -\frac{i}{\hbar} t H \right) \ket{\phi_j} = \exp (-i \omega_j t) \ket{\phi_j}.
$$

したがって、系の初期状態$\ket{\psi (t_0)}$が

$$
\ket{\psi (t_0)} = \sum_{j=0}^{N} c_j \ket{\phi_j}
$$

であれば、時刻$t_1$での状態は

$$
\ket{\psi (t_1)} = \sum_{j=0}^{N} c_j U_H(t_1 - t_0) \ket{\phi_j} = \sum_{j=0}^{N} e^{-i \omega_j (t_1 - t_0)} c_j \ket{\phi_j},
$$

つまり、各固有ベクトルの振幅に、対応する位相因子をかけるだけで求まります。

再び核スピンの例を見ると、初期状態$\ket{\psi(t_0)} = \upket = 1/\sqrt{2} (\rightket + \leftket)$なので、

$$
\begin{align}
\ket{\psi(t_1)} & = \frac{1}{\sqrt{2}} \left( e^{-i\omega (t_1 - t_0)} \rightket + e^{i\omega (t_1 - t_0)} \leftket \right) \\
& = \frac{1}{2} \left[ \left( e^{-i\omega (t_1 - t_0)} + e^{i\omega (t_1 - t_0)} \right) \upket + \left( e^{-i\omega (t_1 - t_0)} - e^{i\omega (t_1 - t_0)} \right) \downket \right] \\
& = \cos [\omega (t_1-t_0)] \upket - i \sin [\omega (t_1-t_0)] \downket
\end{align}
$$

となり、式{eq}`spin_exact`が再導出できます。

このように、ハミルトニアンの対角化さえできれば、量子ダイナミクスのシミュレーションは位相をかけて足し算をするだけの問題に帰着します。しかし、上で言及したように、計算量の問題から、ハミルトニアンが対角化できるのは主に系の次元が小さいときに限ります。「対角化」という言葉が示唆するように、この操作は行列演算（対角化）を伴い、その際の行列の大きさは$N \times N$です。上の核スピンの例では$N=2$でしたが、もっと実用的なシミュレーションの場合、系の量子力学的次元は一般的に関係する自由度の数（粒子数など）の指数関数的に増加します。比較的小規模な系でもすぐに対角化にスーパーコンピュータが必要なスケールになってしまいます。

### 鈴木・トロッター分解

ハミルトニアンが対角化できない場合、ダイナミクスシミュレーションをするには、結局式{eq}`spin_exact`のように初期状態に時間発展演算子を愚直にかけていくことになります。これは、式{eq}`exp_sigmax`のように$U_H(t)$を閉じた形式で厳密に書けるなら簡単な問題ですが、そうでない場合は数値的に近似していく必要があります。その場合の常套手段は、行いたい時間発展$(t_1 - t_0)$を短い時間

$$
\Delta t = \frac{t_1 - t_0}{M}, \quad M \gg 1
$$

に分割し、$\Delta t$だけの時間発展$U_H(\Delta t)$を近似する演算子$\tilde{U}_{H;\Delta t}$を考えることです。この$\tilde{U}_{H;\Delta t}$での状態の遷移の様子がわかるのであれば、それを$M$回繰り返すことで、求める終状態が近似できます。

通常$H$はわかっており、任意の状態$\ket{\psi}$に対して$H\ket{\psi}$が計算できるので、例えば$\mathcal{O}((\Delta t)^2)$を無視する近似で

$$
\tilde{U}_{H;\Delta t} = I - \frac{i \Delta t}{\hbar} H
$$

とすれば、まず$H\ket{\psi(t_0)}$を計算し、それを$i\Delta t/\hbar$倍して$\ket{\psi(t_0)}$から引き、その結果にまた$H$をかけて、…という具合に$\ket{\psi(t_1)}$が近似計算できます[^exact_at_limit]。

しかし、このスキームは量子コンピュータでの実装に向いていません。上で述べたように量子コンピュータのゲートはユニタリ演算子に対応するのに対して、$I - i\Delta t / \hbar H$はユニタリでないからです。代わりに、量子コンピュータでのダイナミクスシミュレーションでよく用いられるのが鈴木・トロッター分解という近似法です{cite}`nielsen_chuang_dynamics`。

鈴木・トロッター分解は、ハミルトニアンを$H = \sum_{k=1}^{L} H_k$と$L$個のエルミート演算子$H_k$の和に分解したときに、

```{math}
:label: trotter_formula
\exp \left(- \frac{i \Delta t}{\hbar} H \right) = \prod_{k=1}^{L} \exp \left(-\frac{i \Delta t}{\hbar} H_k \right) + \mathcal{O}((\Delta t)^2)
```

が成り立つという定理に基づきます。個々の$H_k$について$U_{H_k}(\Delta t) = \exp (-i/\hbar \Delta t H_k)$の量子ゲートによる実装が知られていれば、右辺の第一項が実装できるので、ステップ数$M$で決まる精度でダイナミクスが近似できます。

やや立ち入った話になりますが、この定理の重要なポイントは、一般に線形演算子$A, B$に対して、特殊な条件が満たされる（$A$と$B$が「可換」である）場合を除いて

$$
\exp(A + B) \neq \exp(A)\exp(B)
$$

であるということです。この関係があるからこそ、式{eq}`trotter_formula`で$U_{H_k}(t)$が実装できても$U_H(t)$が実装できるとは限らないのです。しかし、十分短い時間$\Delta t$[^sufficiently_small]についてであれば、$U_H(\Delta t)$を$U_{H_k}(\Delta t)$の積として表しても、その誤差が計算結果に大きく影響しない、ということをこの定理は言っています。

[^exact_at_limit]: 実際、この手続きは$M \rightarrow \infty$の極限で厳密に$U(t_1 - t_0)$による時間発展となります。
[^sufficiently_small]: 具体的には、$\Omega = H/\hbar, \Omega_k = H_k/\hbar$として$\exp(-i\Delta t \Omega) - \prod_{k} \exp(-i\Delta t \Omega_k) = (\Delta t)^2/2 \sum_{k \neq l} [\Omega_k, \Omega_l] + \mathcal{O}((\Delta t)^3)$なので、任意の状態$\ket{\psi}$について$(\Delta t)^2 \sum_{k \neq l} \bra{\psi} [\Omega_k, \Omega_l] \ket{\psi} \ll 1$が成り立つとき、$\Delta t$が十分小さいということになります。

### なぜ量子コンピュータが量子ダイナミクスシミュレーションに向いているか

鈴木・トロッター分解がダイナミクスシミュレーションに適用できるには、ハミルトニアンが都合よくゲートで実装できる$H_k$に分解できる必要があります。これが常に成り立つかというと、答えはyes and noです。

まず、$2^n$次元線形空間に作用するエルミート演算子は、$n$個の2次元部分系に独立に作用する基底演算子$\{I, \sigma^X, \sigma^Y, \sigma^Z\}$の積の線形和に分解できます。$\sigma^X$以外のパウリ演算子$\sigma^Y$と$\sigma^Z$はここまで登場しませんでしたが、重要なのは、2次元量子系に作用する$\sigma^X, \sigma^Y, \sigma^Z$がそれぞれ量子ビットに作用する$X, Y, Z$ゲート[^ygate]に、パウリ演算子の指数関数がそれぞれ$R_x, R_y, R_z$ゲート（総じて回転ゲートと呼びます）に対応するということです。つまり、対象の物理系の量子レジスタへの対応付けさえできれば、そのハミルトニアンは必ず基本的なゲートの組み合わせで表現できます。

しかし、$n$ビットレジスタに作用する基底演算子の組み合わせは$4^n$通りあり、最も一般のハミルトニアンではその全ての組み合わせが寄与することも有りえます。その場合、指数関数的に多くのゲートを用いてしか時間発展演算子が実装できないことになります。それでは「都合よく分解できる」とは言えません。

そもそも量子コンピュータで量子ダイナミクスシミュレーションを行う利点は、その計算効率にあります。

シミュレートする量子系の次元を$2^n$としたとき、古典計算機では、仮にハミルトニアンが対角化できても$2^n$回の位相因子の掛け算と同じ回数だけの足し算を行う必要があります。ハミルトニアンが対角化できず、時間を$M$ステップに区切って近似解を求めるとなると、必要な計算回数は$\mathcal{O}(2^nM)$となります。

一方、同じ計算に$n$ビットの量子コンピュータを使うと、対角化できない場合のステップ数$M$は共通ですが、各ステップで必要な計算回数（＝ゲート数）はハミルトニアン$H$の基底演算子への分解$H_k$の項数$L$で決まります。個々の$H_k$は一般に$\mathcal{O}(n)$ゲート要するので、計算回数は$\mathcal{O}(nLM)$です。したがって、$L$が$\mathcal{O}(1)$であったり$\mathcal{O}(\mathrm{poly}(n))$（$n$の多項式）であったりすれば、量子コンピュータでの計算が古典のケースよりも指数関数的に早いということになります。

したがって、逆に、ハミルトニアンが$4^n$通りの基底演算子に分解されてしまっては（$L=4^n$）、量子コンピュータの利点が活かせません[^exponential_memory]。

幸いなことに、通常我々がシミュレートしたいと思うような物理系では、$L$はせいぜい$\mathcal{O}(n^2)$で、$\mathcal{O}(n)$ということもしばしばあります。2体相互作用のある量子多体系などが前者にあたり、さらに相互作用が隣接した物体間のみである場合、後者が当てはまります。

[^ygate]: $Y$ゲートは変換$Y\ket{0} = i\ket{1}$、$Y\ket{1} = -i\ket{0}$を引き起こします。
[^exponential_memory]: 古典計算機でのシミュレーションでは、一般的には全ての固有ベクトルの振幅を記録しておくためのメモリ（$\mathcal{O}(2^n)$）も必要です。一方量子コンピュータでは（測定時に限られた情報しか取り出せないという問題はありますが）そのような制約がないので、指数関数的に多くのゲートを用いるハミルトニアンでも、一応後者に利点があると言えるかもしれません。

## 実習：ハイゼンベルグモデルの時間発展

### モデルのハミルトニアン

ハミルトニアンの分解と言われてもピンと来ない方もいるかもしれませんし、ここからはダイナミクスシミュレーションの具体例をQiskitで実装してみましょう。

ハイゼンベルグモデルという、磁性体のトイモデルを考えます。空間中一列に固定された多数のスピンを持つ粒子（電子）の系で、隣接スピンの向きによってエネルギーが決まるような問題です。

例えば、$n$スピン系で簡単な形式のハミルトニアンは

```{math}
:label: heisenberg
H = -J \sum_{j=0}^{n-2} (\sigma^X_j\sigma^X_{j+1} + \sigma^Y_j\sigma^Y_{j+1} + \sigma^Z_j \sigma^Z_{j+1})
```

です。ここで、$\sigma^{[X,Y,Z]}_j$は第$j$スピンに作用するパウリ演算子です。

ただし、式{eq}`heisenberg`の和の記法には実は若干の省略があります。例えば第$j$項をより正確に書くと、

$$
I_0 \otimes \dots \otimes I_{j-1} \otimes \sigma^X_j \otimes \sigma^X_{j+1} \otimes I_{j+2} \otimes \dots I_{n-1}
$$

です。ここで$\otimes$は線形演算子間の「テンソル積」を表しますが、聞き慣れない方は掛け算だと思っておいてください。重要なのは、式{eq}`heisenberg`の各項が、上で触れたように$n$個の基底演算子の積になっているということです。さらに、この系では隣接スピン間の相互作用しか存在しないため、ハミルトニアンが$n-1$個の項に分解できています。

この系では、隣接スピン間の向きが揃っている（内積が正）のときにエネルギーが低くなります[^quantum_inner_product]。少し考えるとわかりますが、すべてのスピンが完全に同じ方向を向いている状態が最もエネルギーの低いエネルギー固有状態です。そこで、最低エネルギー状態から少しだけずらして、スピンが一つだけ直角方向を向いている状態を始状態としたときのダイナミクスをシミュレートしてみましょう。

核スピンのケースと同様に、それぞれのスピンについて+$Z$方向を向いた状態$\upket$を量子ビットの状態$\ket{0}$に、-$Z$方向の状態$\downket$を$\ket{1}$に対応づけます。このとき、上で見たように、パウリ演算子$\sigma^X, \sigma^Y, \sigma^Z$と$X, Y, Z$ゲートとが対応します。また、$J=\hbar\omega/2$とおきます。

時間発展演算子は

$$
U_H(t) = \exp \left[ \frac{i\omega t}{2} \sum_{j=0}^{n-2} (\sigma^X_j\sigma^X_{j+1} + \sigma^Y_j\sigma^Y_{j+1} + \sigma^Z_j \sigma^Z_{j+1}) \right]
$$

ですが、ハミルトニアンの各項が互いに可換でないので、シミュレーションでは鈴木・トロッター分解を用いて近似します。各時間ステップ$\Delta t$での近似時間発展は

$$
\tilde{U}_{H;\Delta t} = \prod_{j=0}^{n-2} \exp\left( \frac{i \omega \Delta t}{2} \sigma^X_j\sigma^X_{j+1} \right) \exp\left( \frac{i \omega \Delta t}{2} \sigma^Y_j\sigma^Y_{j+1} \right) \exp\left( \frac{i \omega \Delta t}{2} \sigma^Z_j\sigma^Z_{j+1} \right)
$$

です。

### 量子ゲートでの表現

これを回転ゲートと制御ゲートで表します。まず$\exp(\frac{i \omega \Delta t}{2} \sigma^Z_j\sigma^Z_{j+1})$について考えてみましょう。この演算子の$j$-$(j+1)$スピン系の4つの基底状態への作用は

$$
\begin{align}
\upket_j \upket_{j+1} \rightarrow e^{i \omega \Delta t / 2} \upket_j \upket_{j+1} \\
\upket_j \downket_{j+1} \rightarrow e^{-i \omega \Delta t / 2} \upket_j \downket_{j+1} \\
\downket_j \upket_{j+1} \rightarrow e^{-i \omega \Delta t / 2} \downket_j \upket_{j+1} \\
\downket_j \downket_{j+1} \rightarrow e^{i \omega \Delta t / 2} \downket_j \downket_{j+1}
\end{align}
$$

です。つまり、2つのスピンの「パリティ」（同一かどうか）に応じてかかる位相の符号が違います。

パリティに関する演算をするにはCNOTを使います。例えば以下の回路

[^quantum_inner_product]: これは量子力学的な系なので、もっと正確な表現は「隣接スピン間の内積が正であるようなハミルトニアンの固有状態の固有値が、そうでない固有状態の固有値より小さい」です。

```python
circuit = QuantumCircuit(QuantumRegister(2, 'q'))
circuit.cx(0, 1)
circuit.rz(Parameter(r'-$\omega \Delta t$'), 1)
circuit.cx(0, 1)
circuit.draw('mpl')
```

によって、計算基底$\ket{00}, \ket{01}, \ket{10}, \ket{11}$はそれぞれ

$$
\begin{align}
\ket{00} \rightarrow e^{i \omega \Delta t / 2} \ket{00} \\
\ket{01} \rightarrow e^{-i \omega \Delta t / 2} \ket{01} \\
\ket{10} \rightarrow e^{-i \omega \Delta t / 2} \ket{10} \\
\ket{11} \rightarrow e^{i \omega \Delta t / 2} \ket{11}
\end{align}
$$

と変換するので（確認してください）、まさに$\exp(\frac{i \omega \Delta t}{2} \sigma^Z_j\sigma^Z_{j+1})$の表現になっています。

残りの2つの演算子も同様にパリティに対する回転で表せますが、CNOTで表現できるのは$Z$方向のパリティだけなので、先にスピンを回転させる必要があります。$\exp(\frac{i \omega \Delta t}{2} \sigma^X_j\sigma^X_{j+1})$による変換は

$$
\begin{align}
\rightket_j \rightket_{j+1} \rightarrow e^{i \omega \Delta t / 2} \rightket_j \rightket_{j+1} \\
\rightket_j \leftket_{j+1} \rightarrow e^{-i \omega \Delta t / 2} \rightket_j \leftket_{j+1} \\
\leftket_j \rightket_{j+1} \rightarrow e^{-i \omega \Delta t / 2} \leftket_j \rightket_{j+1} \\
\leftket_j \leftket_{j+1} \rightarrow e^{i \omega \Delta t / 2} \leftket_j \leftket_{j+1}
\end{align}
$$

で、式{eq}`left_right_kets`から、次の回路が対応する変換を引き起こすことがわかります（これも確認してください）。

```python
circuit = QuantumCircuit(QuantumRegister(2, 'q'))
circuit.h(0)
circuit.h(1)
circuit.cx(0, 1)
circuit.rz(Parameter(r'-$\omega \Delta t$'), 1)
circuit.cx(0, 1)
circuit.h(0)
circuit.h(1)
circuit.draw('mpl')
```

最後に、$\exp(\frac{i \omega \Delta t}{2} \sigma^Y_j\sigma^Y_{j+1})$に対応する回路は

```python
circuit = QuantumCircuit(QuantumRegister(2, 'q'))
circuit.p(-np.pi / 2., 0)
circuit.p(-np.pi / 2., 1)
circuit.h(0)
circuit.h(1)
circuit.cx(0, 1)
circuit.rz(Parameter(r'-$\omega \Delta t$'), 1)
circuit.cx(0, 1)
circuit.h(0)
circuit.h(1)
circuit.p(np.pi / 2., 0)
circuit.p(np.pi / 2., 1)
circuit.draw('mpl')
```

です[^sgate]。

### 回路実装

やっと準備が整ったので、シミュレーションを実装しましょう。実機で走らせられるように、$n=5$, $M=10$, $\omega \Delta t = 0.1$とします。上で決めたように、ビット0以外が$\upket$、ビット0が$\rightket$という初期状態から始めます。各$\Delta t$ステップごとに回路のコピーをとり、それぞれのコピーで測定を行うことで、時間発展の様子を観察します。

[^sgate]: $P(\pi/2)$ゲートは$S$ゲートとも呼ばれます。$P(-\pi/2)$は$S^{\dagger}$です。

```python
# まずは全てインポート
import numpy as np
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, Aer, IBMQ, execute
from qiskit.tools.monitor import job_monitor
from qiskit.providers.ibmq import least_busy
from qiskit.ignis.mitigation.measurement import complete_meas_cal, CompleteMeasFitter
# このワークブック独自のモジュール
from utils.dynamics import plot_heisenberg_spins
```

```python
n = 5
M = 10
omegadt = 0.1

circuits = []

circuit = QuantumCircuit(n)

# Bit 0 in state 1/sqrt(2)(|0> + |1>)
circuit.h(0)

for istep in range(M):
    for j in range(n - 1):
        # ZZ
        circuit.cx(j, j + 1)
        circuit.rz(-omegadt, j + 1)
        circuit.cx(j, j + 1)

        # XX
        circuit.h(j)
        circuit.h(j + 1)
        circuit.cx(j, j + 1)
        circuit.rz(-omegadt, j + 1)
        circuit.cx(j, j + 1)
        circuit.h(j)
        circuit.h(j + 1)

        # YY
        circuit.p(-np.pi / 2., j)
        circuit.p(-np.pi / 2., j + 1)
        circuit.h(j)
        circuit.h(j + 1)
        circuit.cx(j, j + 1)
        circuit.rz(-omegadt, j + 1)
        circuit.cx(j, j + 1)
        circuit.h(j)
        circuit.h(j + 1)
        circuit.p(np.pi / 2., j)
        circuit.p(np.pi / 2., j + 1)

    # Save a copy of the circuit at this point
    # measure_all(inplace=False) creates a copy of the circuit with a full measurement at the end
    circuits.append(circuit.measure_all(inplace=False))

print('{} circuits created'.format(len(circuits)))
```

量子回路シミュレーターで実行し、各ビットにおける$Z$方向スピンの期待値をプロットしましょう。プロット用の関数は比較的長くなってしまいますが実習の本質とそこまで関係しないので、[別ファイル](https://github.com/UTokyo-ICEPP/qc-workbook/blob/master/source/utils/dynamics.py)に定義してあります。関数はジョブの実行結果、系のスピンの数、初期状態、ステップ間隔を引数にとります。

```python
# Define the initial statevector
initial_state = np.zeros(2 ** n, dtype=np.complex128)
initial_state[0:2] = np.sqrt(0.5)

shots = 100000

qasm_simulator = Aer.get_backend('qasm_simulator')

sim_job = execute(circuits, qasm_simulator, shots=shots)
sim_counts_list = sim_job.result().get_counts()

plot_heisenberg_spins(sim_counts_list, n, initial_state, omegadt, add_theory_curve=True)
```

ビット0でのスピンの不整合が徐々に他のビットに伝搬していく様子が観察できました。

また、上のように関数`plot_heisenberg_spins`に`add_theory_curve=True`という引数を渡すと、ハミルトニアンを対角化して計算した厳密解のカーブも同時にプロットします。トロッター分解による解が、厳密解から少しずつずれていっている様子も観察できます。興味があれば$\Delta t$を小さく（$M$を大きく）して、ずれがどう変わるか確認してみてください。

実機でも同様の結果が得られるでしょうか。QV32の5量子ビットマシンで確認してみましょう。

```python
IBMQ.enable_account('__paste_your_token_here__')
provider = IBMQ.get_provider(hub='ibm-q', group='open', project='main')

backend_filter = lambda b: (not b.configuration().simulator) and b.configuration().n_qubits >= n and b.configuration().quantum_volume >= 32 and b.status().operational
backend = least_busy(provider.backends(filters=backend_filter))

job = execute(circuits, backend, shots=8192, optimization_level=3)

job_monitor(job, interval=2)

counts_list = job.result().get_counts()
```

```python
plot_heisenberg_spins(counts_list, n, initial_state, omegadt)
```

(measurement_error_mitigation)=
### おまけ：測定エラーの緩和

{doc}`第二回の課題 <quantum_computation>`でも触れましたが、実機ではCNOTゲートと測定で主にエラーが起きます。課題ではCNOTを極力減らす回路を書きましたが、今回のシミュレーションではなかなかこれ以上CNOTを減らす工夫のしようがなさそうです。

そこで、今度は測定エラーに注目します。測定エラーは統計的にだいたい再現性がある（あるビット列$x$が得られるべき状態から別のビット列$y$が得られる確率が、状態の生成法に依存しにくい）ので、事後的に緩和（部分的補正）できます。そのためには$n$ビットレジスタの$2^n$個すべての計算基底状態について、相当するビット列が100%の確率で得られるべき回路を作成し、それを測定した結果を利用します。

例えば$n=2$で状態$\ket{x} \, (x = 00, 01, 10, 11)$を測定してビット列$y$を得る確率が$\epsilon^x_y$だとします。このとき実際の量子計算をして測定で得られた確率分布が$\{p_y\}$であったとすると、その計算で本来得られるべき確率分布$\{P_x\}$は連立方程式

$$
p_{00} = P_{00} \epsilon^{00}_{00} + P_{01} \epsilon^{01}_{00} + P_{10} \epsilon^{10}_{00} + P_{11} \epsilon^{11}_{00} \\
p_{01} = P_{00} \epsilon^{00}_{01} + P_{01} \epsilon^{01}_{01} + P_{10} \epsilon^{10}_{01} + P_{11} \epsilon^{11}_{01} \\
p_{10} = P_{00} \epsilon^{00}_{10} + P_{01} \epsilon^{01}_{10} + P_{10} \epsilon^{10}_{10} + P_{11} \epsilon^{11}_{10} \\
p_{11} = P_{00} \epsilon^{00}_{11} + P_{01} \epsilon^{01}_{11} + P_{10} \epsilon^{10}_{11} + P_{11} \epsilon^{11}_{11}
$$

を解けば求まります。つまり、行列$\epsilon^x_y$の逆をベクトル$p_y$にかければいいわけです[^actually_fits]。

Qiskitでは測定エラー緩和用の関数やクラスが提供されているので、それを使って実際にエラーを求め、上のシミュレーションの結果の改善を試みましょう。

[^actually_fits]: 実際には数値的安定性などの理由から、単純に逆行列をかけるわけではなくフィッティングが行われますが、発想はここで書いたものと変わりません。

```python
# Define 32 calibration measurement circuits from a 5-qubit register
qreg = QuantumRegister(5)
meas_calibs, state_labels = complete_meas_cal(qr=qreg, circlabel='mcal')

# Run the circuits
calib_job = execute(meas_calibs, backend, shots=8192)

job_monitor(calib_job, interval=2)

calib_result = calib_job.result()
```

```python
# Create an error corrector object
meas_fitter = CompleteMeasFitter(calib_result, state_labels, circlabel='mcal')

# Get the filter object
meas_filter = meas_fitter.filter

# Results with mitigation
mitigated_counts_list = []
for counts in counts_list:
    mitigated_counts_list.append(meas_filter.apply(counts))

plot_heisenberg_spins(mitigated_counts_list, n, initial_state, omegadt)
```

違いはみられたでしょうか？（劇的な違いは期待できません。）

## 参考文献

```{bibliography}
:filter: docname in docnames
```


Table of Contents

1  Objectives

2  Motivating  The Bayesian Classifier 🧐

2.1  Naive Bayes Setup

2.1.1  What's So Great About This?

2.2  The Naive Assumption

2.3  The Formula

2.3.1  What Parts Can We Find?

2.4  Calculating That Our Email Is Spam

2.5  Extending It With Multiple Words

3  Naive Bayes Modeling Example

3.1  Using Bayes's Theorem for Classification

3.1.1  Does this look like a classification problem?

3.2  Elephant Example

3.2.1  Naive Bayes by Hand

3.2.2  Calculation of Likelihoods

3.2.3  Posteriors

3.2.4  More Dimensions

3.2.4.1  What's "Naive" about This?

3.2.4.2  Posteriors

3.2.5  GaussianNB

3.3  Comma Survey Example

3.3.1  Calculating Priors and Likelihoods

3.3.2  Calculating Posteriors

3.3.3  Comparison with MultinomialNB

```python
import numpy as np
import pandas as pd
from scipy import stats
from sklearn.naive_bayes import MultinomialNB, GaussianNB
    # There is also a BernoulliNB for a dataset with binary predictors
import seaborn as sns
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import plot_confusion_matrix
from sklearn.preprocessing import OneHotEncoder

%matplotlib inline
```

# Objectives

- Describe how Bayes's Theorem can be used to make predictions of a target
- Identify the appropriate variant of Naive Bayes models for a particular business problem

# Motivating  The Bayesian Classifier 🧐

> Let's take a second to go through an example to get a feel for how Bayes' Theorem can help us with classification. Specifically about document classification

Spam, Spam, Spam, Spam, Spam...

> This is the classic example: detecting email spam!

**The Problem Setup**

> We get emails that can be either emails we care about (***ham*** 🐷) or emails we don't care about (***spam*** 🥫).
>
> We can probably look at the words in the email and get an idea of whether they are spam or not just by observing if they contain red-flag words 🚩
>
> We won't always be right, but if we see an email that uses word(s) that are more often associated with spam, then we can feel more confident as labeling that email as spam!

## Naive Bayes Setup

What we gotta do:

1. Look at spam and not spam (ham) emails
2. Identify words that suggest classification
3. Determine probability that words occur in each classification
4. Profit (classify new emails as "spam" or "ham")

### What's So Great About This?

- We can keep updating our belief based on the emails we detect
- Relatively simple
- Can expand to multiple words

## The Naive Assumption

$P(A,B) = P(A\cap B) = P(A)\ P(B)$ only if independent

In practice, makes sense & is usually pretty good assumption

## The Formula

Let's say the word that occurs is "cash":

$$ P(🥫 | "cash") = \frac{P("cash" | 🥫)P(🥫)}{P("cash")}$$

### What Parts Can We Find?

- $P("cash")$
    * That's just the probability of finding the word "cash"! Frequency of the word!
- $P(🥫)$
    * Well, we start with some data (_prior knowledge_). So frequency of the spam occurring!
- $P("cash" | 🥫)$
    * How frequently "cash" is used in known spam emails. Count the frequency across all spam emails

## Calculating That Our Email Is Spam

```python
# Let's just say 2% of all emails have the word "cash" in them
p_cash = 0.02

# We normally would measure this from our data, but we'll take
# it that 10% of all emails we collected were spam
p_spam = 0.10

# 12% of all spam emails have the word "cash"
p_cash_given_its_spam = 0.12
```

```python
p_spam_given_cash = p_cash_given_its_spam * p_spam / p_cash
print(f'If the email has the word "cash" in it, there is a \
{p_spam_given_cash*100}% chance the email is spam')
```

    If the email has the word "cash" in it, there is a 60.0% chance the email is spam

> **Check it**: Does this make sense?

> Suppose I had 250 total emails.
> - How many should I expect to have the word 'cash' in them? (Ans. 5)
> - How many should I expect to be spam? (Ans. 25)
> - How many *of the spam emails* should I expect to have the word 'cash' in them? (Ans. 3)

## Extending It With Multiple Words

> With more words, the more certain we can be if it is/isn't spam

Spam:

$$ P(🥫\ |"buy",\ "cash") \propto P("buy",\ "cash"|\ 🥫)\ P(🥫)$$

But because of independence:

$$ P("buy",\ "cash"|\ 🥫) = P("buy"|\ 🥫)\ P("cash"|\ 🥫)$$

Normalize by dividing!

$$
P(🥫\ |"buy",\ "cash")  =
    \frac
        {P("buy"|\ 🥫)P("cash"|\ 🥫)\ P(🥫)}
        {P("buy"|\ 🥫)P("cash"|\ 🥫)\ P(🥫) + P("buy"|\ 🐷)P("cash"|\ 🐷)\ P(🐷)}
$$

> **Note:** If we wanted to find the most probable class (especially useful for _multiclass_), we find the maximum numerator for the given criteria

# Naive Bayes Modeling Example

## Using Bayes's Theorem for Classification

Let's recall Bayes's Theorem:

$\large P(h|e) = \frac{P(h)P(e|h)}{P(e)}$

### Does this look like a classification problem?

- Suppose we have three competing hypotheses $\{h_1, h_2, h_3\}$ that would explain our evidence $e$.
    - Then we could use Bayes's Theorem to calculate the posterior probabilities for each of these three:
        - $P(h_1|e) = \frac{P(h_1)P(e|h_1)}{P(e)}$
        - $P(h_2|e) = \frac{P(h_2)P(e|h_2)}{P(e)}$
        - $P(h_3|e) = \frac{P(h_3)P(e|h_3)}{P(e)}$

- Suppose the evidence is a collection of elephant weights.
- Suppose each of the three hypotheses claims that the elephant whose measurements we have belongs to one of the three extant elephant species (*L. africana*, *L. cyclotis*, and *E. maximus*).

In that case the left-hand sides of these equations represent the probability that the elephant in question belongs to a given species.

If we think of the species as our target, then **this is just an ordinary classification problem**.

What about the right-hand sides of the equations? **These other probabilities we can calculate from our dataset.**

- The priors can simply be taken to be the percentages of the different classes in the dataset.
- What about the likelihoods?
    - If the relevant features are **categorical**, we can simply count the numbers of each category in the dataset. For example, if the features are whether the elephant has tusks or not, then, to calculate the likelihoods, we'll just count the tusked and non-tuksed elephants per species.
    - If the relevant features are **numerical**, we'll have to do something else. A good way of proceeding is to rely on (presumed) underlying distributions of the data. [Here](https://medium.com/analytics-vidhya/use-naive-bayes-algorithm-for-categorical-and-numerical-data-classification-935d90ab273f) is an example of using the normal distribution to calculate likelihoods. We'll follow this idea below for our elephant data.

## Elephant Example

Suppose we have a dataset that looks like this:

```python
elephs = pd.read_csv('data/elephants.csv', usecols=['height (cm)',
                                                   'species'])
```

```python
elephs.head()
```

      height (cm)
      species

      0
      231.683867
      maximus

      1
      277.714843
      cyclotis

      2
      268.228131
      africana

      3
      267.334322
      cyclotis

      4
      270.165582
      maximus

```python
plt.style.use('fivethirtyeight')

fig, ax = plt.subplots()

sns.kdeplot(data=elephs[elephs['species'] == 'maximus']['height (cm)'],
            ax=ax, label='maximus')
sns.kdeplot(data=elephs[elephs['species'] == 'africana']['height (cm)'],
            ax=ax, label='africana')
sns.kdeplot(data=elephs[elephs['species'] == 'cyclotis']['height (cm)'],
            ax=ax, label='cyclotis')

plt.legend();
```

### Naive Bayes by Hand

Suppose we want to make prediction of species for some new elephant whose weight we've just recorded. We'll suppose the new elephant has:

```python
new_ht = 263
```

What we want to calculate is the mean and standard deviation for height for each elephant species. We'll use these to calculate the relevant likelihoods.

So:

```python
max_stats = elephs[elephs['species'] == 'maximus'].describe().loc[['mean', 'std'], :]
max_stats
```

      height (cm)

      mean
      256.143764

      std
      18.270532

```python
cyc_stats = elephs[elephs['species'] == 'cyclotis'].describe().loc[['mean', 'std'], :]
cyc_stats
```

      height (cm)

      mean
      278.993125

      std
      14.669292

```python
afr_stats = elephs[elephs['species'] == 'africana'].describe().loc[['mean', 'std'], :]
afr_stats
```

      height (cm)

      mean
      286.642016

      std
      17.127223

```python
elephs['species'].value_counts()
```

    africana    50
    cyclotis    50
    maximus     50
    Name: species, dtype: int64

### Calculation of Likelihoods

We'll use the PDFs of the normal distributions with the discovered means and standard deviations to calculate likelihoods:

```python
stats.norm(loc=max_stats['height (cm)'][0],
           scale=max_stats['height (cm)'][1]).pdf(263)
```

    0.02035072428154637

```python
stats.norm(loc=cyc_stats['height (cm)'][0],
          scale=cyc_stats['height (cm)'][1]).pdf(263)
```

    0.015010397188804918

```python
stats.norm(loc=afr_stats['height (cm)'][0],
          scale=afr_stats['height (cm)'][1]).pdf(263)
```

    0.00898384467843682

### Posteriors

What we have just calculated are (approximations of) the likelihoods, i.e.:

- $P(height=263 | species=maximus) = 2.04\%$
- $P(height=263 | species=cyclotis) = 1.50\%$
- $P(height=263 | species=africana) = 0.90\%$

(Notice that they do NOT sum to 1!) But what we'd really like to know are the posteriors. I.e. what are:

- $P(species=maximus | height=263)$?
- $P(species=cyclotis | height=263)$?
- $P(species=africana | height=263)$?

Since we have equal numbers of each species, every prior is equal to $\frac{1}{3}$. Thus we can calculate the probability of the evidence:

$P(height=263) = \frac{1}{3}(0.0204 + 0.0150 + 0.0090) = 0.0148$

And therefore calculate the posteriors using Bayes's Theorem:

- $P(species=maximus | height=263) = \frac{1}{3}\frac{0.0204}{0.0148} = 45.9\%$;
- $P(species=cyclotis | height=263) = \frac{1}{3}\frac{0.0150}{0.0148} = 33.8\%$;
- $P(species=africana | height=263) = \frac{1}{3}\frac{0.0090}{0.0148} = 20.3\%$.

Bayes's Theorem shows us that the largest posterior belongs to the *maximus* species. (Note also that, since the priors are all the same, the largest posterior will necessarily belong to the species with the largest likelihood!)

Therefore, the *maximus* species will be our prediction for an elephant of this height.

### More Dimensions

In fact, we also have elephant *weight* data available in addition to their heights. To accommodate multiple features we can make use of **multivariate normal** distributions.

#### What's "Naive" about This?

For multiple predictors, we make the simplifying assumption that **our predictors are probablistically independent**. This will often be unrealistic, but it simplifies our calculations a great deal.

```python
elephants = pd.read_csv('data/elephants.csv',
                       usecols=['height (cm)', 'weight (lbs)', 'species'])
```

```python
elephants.head()
```

      height (cm)
      weight (lbs)
      species

      0
      231.683867
      6469.491895
      maximus

      1
      277.714843
      7639.806953
      cyclotis

      2
      268.228131
      8561.517693
      africana

      3
      267.334322
      7673.608014
      cyclotis

      4
      270.165582
      7259.901759
      maximus

```python
maximus = elephants[elephants['species'] == 'maximus']
cyclotis = elephants[elephants['species'] == 'cyclotis']
africana = elephants[elephants['species'] == 'africana']
```

Suppose our new elephant with a height of 263 cm also has a weight of 7009 lbs.

```python
likeli_max = stats.multivariate_normal(mean=maximus.mean(),
                          cov=maximus.cov()).pdf([263, 7009])
likeli_max
```

    1.0464392596305256e-05

```python
likeli_cyc = stats.multivariate_normal(mean=cyclotis.mean(),
                         cov=cyclotis.cov()).pdf([263, 7009])
likeli_cyc
```

    5.811801818372348e-07

```python
likeli_afr = stats.multivariate_normal(mean=africana.mean(),
                         cov=africana.cov()).pdf([263, 7009])
likeli_afr
```

    4.391536207783361e-08

#### Posteriors

```python
post_max = likeli_max / sum([likeli_max, likeli_cyc, likeli_afr])
post_cyc = likeli_cyc / sum([likeli_max, likeli_cyc, likeli_afr])
post_afr = likeli_afr / sum([likeli_max, likeli_cyc, likeli_afr])

print(post_max)
print(post_cyc)
print(post_afr)
```

    0.9436317045465862
    0.0524082062660187
    0.003960089187395182

### [`GaussianNB`](https://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.GaussianNB.html)

```python
gnb = GaussianNB(priors=[1/3, 1/3, 1/3])
```

```python
X = elephants.drop('species', axis=1)
y = elephants['species']

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
gnb.fit(X_train, y_train)
```

    GaussianNB(priors=[0.3333333333333333, 0.3333333333333333, 0.3333333333333333])

```python
gnb.predict_proba(np.array([263, 7009]).reshape(1, -1))
```

    array([[0.00425713, 0.04799459, 0.94774828]])

```python
gnb.score(X_test, y_test)
```

    0.8947368421052632

```python
plot_confusion_matrix(gnb, X_test, y_test);
```

## Comma Survey Example

```python
commas = pd.read_csv('data/comma-survey.csv')
```

```python
commas.head()
```

      RespondentID
      In your opinion, which sentence is more gramatically correct?
      Prior to reading about it above, had you heard of the serial (or Oxford) comma?
      How much, if at all, do you care about the use (or lack thereof) of the serial (or Oxford) comma in grammar?
      How would you write the following sentence?
      When faced with using the word "data", have you ever spent time considering if the word was a singular or plural noun?
      How much, if at all, do you care about the debate over the use of the word "data" as a singluar or plural noun?
      In your opinion, how important or unimportant is proper use of grammar?
      Gender
      Age
      Household Income
      Education
      Location (Census Region)

      0
      3292953864
      It's important for a person to be honest, kind...
      Yes
      Some
      Some experts say it's important to drink milk,...
      No
      Not much
      Somewhat important
      Male
      30-44
      $50,000 - $99,999
      Bachelor degree
      South Atlantic

      1
      3292950324
      It's important for a person to be honest, kind...
      No
      Not much
      Some experts say it's important to drink milk,...
      No
      Not much
      Somewhat unimportant
      Male
      30-44
      $50,000 - $99,999
      Graduate degree
      Mountain

      2
      3292942669
      It's important for a person to be honest, kind...
      Yes
      Some
      Some experts say it's important to drink milk,...
      Yes
      Not at all
      Very important
      Male
      30-44
      NaN
      NaN
      East North Central

      3
      3292932796
      It's important for a person to be honest, kind...
      Yes
      Some
      Some experts say it's important to drink milk,...
      No
      Some
      Somewhat important
      Male
      18-29
      NaN
      Less than high school degree
      Middle Atlantic

      4
      3292932522
      It's important for a person to be honest, kind...
      No
      Not much
      Some experts say it's important to drink milk,...
      No
      Not much
      NaN
      NaN
      NaN
      NaN
      NaN
      NaN

The first question on the survey was about the Oxford comma.

```python
commas.isna().sum().sum()
```

    924

We'll go ahead and drop the NaNs:

```python
commas = commas.dropna()
```

```python
commas.shape
```

    (825, 13)

```python
commas['In your opinion, which sentence is more gramatically correct?'].value_counts()
```

    It's important for a person to be honest, kind, and loyal.    470
    It's important for a person to be honest, kind and loyal.     355
    Name: In your opinion, which sentence is more gramatically correct?, dtype: int64

Personally, I like the Oxford comma, since it can help eliminate ambiguities, such as:

"This book is dedicated to my parents, Ayn Rand, and God"
 vs.

"This book is dedicated to my parents, Ayn Rand and God"

Let's see how a Naive Bayes model would make a prediction here. We'll think of the comma preference as our target.

```python
commas['Age'].value_counts()
```

    45-60    248
    30-44    205
    > 60     199
    18-29    173
    Name: Age, dtype: int64

Suppose we want to make a prediction about Oxford comma usage for a new person who falls into the **45-60 age group**.

### Calculating Priors and Likelihoods

The following code makes a table of values that count up the number of survey respondents who fall into each of eight bins (the four age groups and the two answers to the first comma question).

```python
table = np.zeros((2, 4))

for idx, value in enumerate(commas['Age'].value_counts().index):
    table[0, idx] = len(commas[(commas['In your opinion, which sentence is '\
                                       'more gramatically correct?'] ==\
                                        'It\'s important for a person to be '\
                                'honest, kind, and loyal.') & (commas['Age'] == value)])
    table[1, idx] = len(commas[(commas['In your opinion, which sentence is '\
                                       'more gramatically correct?'] ==\
                                        'It\'s important for a person to be '\
                                'honest, kind and loyal.') & (commas['Age'] == value)])
```

```python
table
```

    array([[123., 121.,  90., 136.],
           [125.,  84., 109.,  37.]])

```python
df = pd.DataFrame(table, columns=['Age45-60',
                            'Age>60',
                            'Age30-44',
                            'Age18-29'])
df
```

      Age45-60
      Age>60
      Age30-44
      Age18-29

      0
      123.0
      121.0
      90.0
      136.0

      1
      125.0
      84.0
      109.0
      37.0

```python
df['Oxford'] = [True, False]
df = df[['Age>60', 'Age45-60', 'Age30-44', 'Age18-29', 'Oxford']]
df
```

      Age>60
      Age45-60
      Age30-44
      Age18-29
      Oxford

      0
      121.0
      123.0
      90.0
      136.0
      True

      1
      84.0
      125.0
      109.0
      37.0
      False

Since all we have is a single categorical feature here we can just read our likelihoods and priors right off of this table:

Likelihoods:

- Age45-60:
    - P(Age45-60 | Oxford=True) = $\frac{123}{470} = 0.2617$;
    - P(Age45-60 | Oxford=False) = $\frac{125}{355} = 0.3521$.

Priors:

- P(Oxford=True) = $\frac{470}{825} = 0.5697$;
- P(Oxford=False) = $\frac{355}{825} = 0.4303$.

### Calculating Posteriors

First we'll calculate the probability of the evidence:

$$\begin{align}
    P(Age45-60) &= P(Age45-60 | Oxford=True) \cdot P(Oxford=True) \\
                & \hspace{1cm} + P(Age45-60 | Oxford=False) \cdot P(Oxford=False)\\
                &= 0.2617 \cdot 0.5697 + 0.3521 \cdot 0.4303 \\
                &= 0.3006
\end{align}$$

```python
(123+125)/825
```

    0.3006060606060606

Now use Bayes's Theorem to calculate the posteriors:

$$\begin{align}
P(Oxford=True | Age45-60) &= P(Oxford=True) \cdot P(Age45-60 | Oxford=True) / P(Age45-60) \\
                          &= 0.5697 \cdot 0.2617 / 0.3006 \\
                          &= 0.4960 \\
                          \\
P(Oxford=False | Age45-60) &= P(Oxford=False) \cdot P(Age45-60 | Oxford=False) / P(Age45-60) \\
                          &= 0.4303 \cdot 0.3521 / 0.3006 \\
                          &= 0.5040
\end{align}$$

Close! But our prediction for someone in the 45-60 age group will be that they **do not** favor the Oxford comma.

### Comparison with [`MultinomialNB`](https://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.MultinomialNB.html)

```python
comma_model = MultinomialNB()

ohe = OneHotEncoder()
ohe.fit(commas['Age'].values.reshape(-1, 1))

X = ohe.transform(commas['Age'].values.reshape(-1, 1)).todense()
y = commas['In your opinion, which sentence is more gramatically correct?']
```

```python
comma_model.fit(X, y)
```

    MultinomialNB()

```python
comma_model.predict_proba(np.array([0, 0, 1, 0]).reshape(1, -1))
```

    array([[0.50331754, 0.49668246]])

```python

```


```python
from sympy import init_session
init_session()
```

    IPython console for SymPy 1.5.1 (Python 3.6.9-64-bit) (ground types: gmpy)

    These commands were executed:
    >>> from __future__ import division
    >>> from sympy import *
    >>> x, y, z, t = symbols('x y z t')
    >>> k, m, n = symbols('k m n', integer=True)
    >>> f, g, h = symbols('f g h', cls=Function)
    >>> init_printing()

    Documentation can be found at https://docs.sympy.org/1.5.1/

```python
init_printing?
```

```python
# this is for rac[3,1]
a, b, d, k, l0 = symbols('a b d k, l0')
```

```python
f=l0*(k**2 +2*a**2*k + a**4+b**2)*(1+d**2*k)/(a**4 + b**2 + k*(2*a**2 + d**2*(a**4+b**2)))
f
```

```python
f.factor()
```

```python
diff(f,l0)
```

```python
da=diff(f,a)
da.factor()
```

```python
db=diff(f,b)
db.factor()
```

$\displaystyle - \frac{2 b k^{2} l_{0} \left(d^{2} k + 1\right) \left(2 a^{2} d^{2} + d^{2} k + 1\right)}{\left(a^{4} d^{2} k + a^{4} + 2 a^{2} k + b^{2} d^{2} k + b^{2}\right)^{2}}$

```python
dd=diff(f,d)
dd.factor()
```

$\displaystyle \frac{4 a^{2} d k^{2} l_{0} \left(a^{4} + 2 a^{2} k + b^{2} + k^{2}\right)}{\left(a^{4} d^{2} k + a^{4} + 2 a^{2} k + b^{2} d^{2} k + b^{2}\right)^{2}}$

```python
printing.sstrrepr(f.factor())
```

    'l0*(d**2*k + 1)*(a**4 + 2*a**2*k + b**2 + k**2)/(a**4*d**2*k + a**4 + 2*a**2*k + b**2*d**2*k + b**2)'

```python
printing.sstrrepr(diff(f,l0).factor())
```

    '(d**2*k + 1)*(a**4 + 2*a**2*k + b**2 + k**2)/(a**4*d**2*k + a**4 + 2*a**2*k + b**2*d**2*k + b**2)'

```python
printing.sstrrepr(da.factor())
```

    '-4*a*k**2*l0*(d**2*k + 1)*(a**4*d**2 + a**2*d**2*k + a**2 - b**2*d**2 + k)/(a**4*d**2*k + a**4 + 2*a**2*k + b**2*d**2*k + b**2)**2'

```python
printing.sstrrepr(db.factor())
```

    '-2*b*k**2*l0*(d**2*k + 1)*(2*a**2*d**2 + d**2*k + 1)/(a**4*d**2*k + a**4 + 2*a**2*k + b**2*d**2*k + b**2)**2'

```python
printing.sstrrepr(dd.factor())
```

    '4*a**2*d*k**2*l0*(a**4 + 2*a**2*k + b**2 + k**2)/(a**4*d**2*k + a**4 + 2*a**2*k + b**2*d**2*k + b**2)**2'

```python

```


```python
from vpython import *

ball = sphere(pos=vector(-5,0,0), radius=0.5,color=color.cyan)
wallR = box(pos=vector(6,0,0), size=vector(0.2,12,12), color=color.green)
ball.velocity = vector(25,0,0)
deltat = 0.005
t = 0
ball.pos = ball.pos + ball.velocity*deltat
while t < 3:
    rate(100)
    if ball.pos.x > wallR.pos.x:
        ball.velocity.x = -ball.velocity.x
    ball.pos = ball.pos + ball.velocity*deltat
    t = t + deltat
```

[Image1]: ./Images/Spring-Pendulum.png "Problem diagram"

# Spring - Pendulum

![Problem diagram][Image1]

### Lagrangian Function

Taking the roof as the zero of gravitational potential ($V_{g} = 0$), we that the gravitational potential energy $V_{g}$ takes the form:

$$ V_{g} = - m g (L_{0} + L) \cos{\theta} $$

Where $L$ is the spring's elongation and $L_{0}$ is the spring's own length. Furthermore, the elastic energy $V_{k}$ associated with the spring's elongation takes the form:

$$ V_{k} = \frac{1}{2} k L^{2} $$

Moreover, the kinetic energy $T$ is:

$$ T = \frac{1}{2} m \left(\dot{r}^{2} + r^{2} \dot{\theta}^{2} \right) = \frac{1}{2} m \; \left( \dot{L}^{2} + (L_{0} + L)^{2} \dot{\theta}^{2} \right)$$

Where we have considered that $r = L_{0} + L$, so $\dot{r} = \dot{L} $. Then, the Lagrangian finally takes the form:

$$ \mathscr{L} = T - V = T - V_{g} - V_{k} = \frac{1}{2} m \; \left( \dot{L}^{2} + (L_{0} + L)^{2} \dot{\theta}^{2} \right) + m g (L_{0} + L) \cos{\theta} - \frac{1}{2} k L^{2} $$

### Equations of motion

Then the equations of motion are:

$$\frac{d}{dt} \left( \frac{\partial \mathscr{L}}{\partial \dot{L}} \right) - \frac{\partial \mathscr{L}}{\partial L} = 0 \quad \implies \quad \ddot{L} = (L_{0} + L) \dot{\theta}^{2} + g \cos{\theta} - \frac{k}{m} L$$

$$\frac{d}{dt} \left( \frac{\partial \mathscr{L}}{\partial \dot{\theta}} \right) - \frac{\partial \mathscr{L}}{\partial \theta} = 0 \quad \implies \quad \ddot{\theta} = - \frac{1}{(L_{0} + L)} \left[ \; g \sin{\theta} + 2 \dot{L} \dot{\theta} \; \right]$$

### Solve the system numerically

As a first approach, we will try to solve the system numerically. To make it, we have to reduce the order of the differential equations, changing from two differential equations of second order to four differential equations of first order.

Let:

$$ v = \dot{L} $$

$$ \omega = \dot{\theta} $$

So:

$$ \dot{L} = f_{1} (L, \theta, v, \omega, \mathrm{params}) = v $$

$$ \dot{\theta} = f_{2} (L, \theta, v, \omega, \mathrm{params}) = \omega$$

$$ \dot{v} = f_{3} (L, \theta, v, \omega, \mathrm{params}) = (L_{0} + L) \omega^{2} + g \cos{\theta} - \frac{k}{m} L $$

$$ \dot{\omega} = f_{4} (L, \theta, v, \omega, \mathrm{params}) = - \frac{1}{(L_{0} + L)} \left[ \; g \sin{\theta} + 2 v \omega \; \right] $$

Where we are working in space $( \; L \; , \; \theta \; , \; v \; , \; \omega \; )$ and $ \mathrm{params} = [ m, g, k, L_{0}]$

In order to plot real motion of the mass, take into account that:

$$ x = (L_{0} + L) \sin{\theta}$$

$$ y = - (L_{0} + L) \cos{\theta}$$

```python
#Libraries
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from scipy import signal
```

```python
# Functions for solving differential equations and to define fluxes in phase portrait

def f1(L, theta, v, omega, m, g, k, Lo):
    return v

def f2(L, theta, v, omega, m, g, k, Lo):
    return omega

def f3(L, theta, v, omega, m, g, k, Lo):
    return (Lo + L) * omega**2 + g * np.cos(theta) - k/m * L

def f4(L, theta, v, omega, m, g, k, Lo):
    return - (g * np.sin(theta) + 2.0 * v * omega) / (Lo + L)

def dydt(y, t, m, g, k, Lo):

    L, theta, v, omega = y

    dL = f1(L, theta, v, omega, m, g, k, Lo)
    dtheta = f2(L, theta, v, omega, m, g, k, Lo)
    dv = f3(L, theta, v, omega, m, g, k, Lo)
    domega = f4(L, theta, v, omega, m, g, k, Lo)

    return [dL, dtheta, dv, domega]
```

Let's intend to search some interresting dynamic regime in the system, trying out with some values for the system's parameters

```python
# Constant parameters
m = 0.2
g = 9.8
k = 3.5
Lo = 1

# Initial conditions
L0 = Lo
v0 = 0.
theta0 =  0.3
omega0 =  0.

y0 = [L0, theta0, v0, omega0]

# Time
ti = 0
tf = 25
Nt = 1000

t, timestep = np.linspace(ti, tf , Nt, retstep=True)

print('time step = {}'.format(timestep))

# Solve differential equations
sol = odeint(dydt, y0, t, args=(m, g, k, Lo))

# Retrieve variales information
L = sol[:, 0]
theta = sol[:, 1]
v = sol[:, 2]
omega = sol[:, 3]

# Retrieve (x,y) positions
x = (Lo + L) * np.sin(theta)
y = -(Lo + L) * np.cos(theta)
```

    time step = 0.025025025025025027

```python
# Plot positions

plt.close()
plt.figure(figsize=(8,8))
plt.plot(x, y,"-")
plt.xlabel('y')
plt.ylabel('x')
plt.title('Real motion')
plt.axis([-np.abs(x).max(), np.abs(x).max(), -np.abs(y).max(), 0]) # plt.axis([xmin, xmax, ymin, ymax])
plt.grid()
plt.show()
```

```python
# Plot time series

plt.close()
plt.title(r'$L$ time series')
plt.plot(t, L,"-")
plt.xlabel(r'$t$')
plt.ylabel(r'$L$')
plt.grid()
plt.show()

plt.close()
plt.title(r'$\theta$ time series')
plt.plot(t, theta,"-")
plt.xlabel(r'$t$')
plt.ylabel(r'$\theta$')
plt.grid()
plt.show()
```

```python
# Power spectrum and Power spectrum density for L time series.

time_series = L
# fourier = np.fft.fft(time_series)
# n = time_series.size

print('Nyquist frecuency = {}'.format(1./(2.*timestep)))

plt.close()
f, Pxx = signal.periodogram(time_series, 1./timestep, scaling='spectrum')
plt.semilogy(f, Pxx)
plt.title(r'$L$ Power Spectrum (log scale y)')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()

plt.close()
f, Pxx = signal.periodogram(time_series, 1./timestep, scaling='spectrum')
plt.plot(f, Pxx,'-')
plt.title(r'$L$ Power Spectrum')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()

plt.close()
f, Pxx_den = signal.periodogram(time_series, 1./timestep, scaling='density')
plt.semilogy(f, Pxx_den)
plt.title(r'$L$ Power Spectrum Density (log scale y)')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()

plt.close()
f, Pxx_den = signal.periodogram(time_series, 1./timestep, scaling='density')
plt.plot(f, Pxx_den, '-')
plt.title(r'$L$ Power Spectrum Density')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()
```

```python
# Power spectrum and Power spectrum density for THETA time series.

time_series = theta

plt.close()
f, Pxx = signal.periodogram(time_series, 1./timestep, scaling='spectrum')
plt.semilogy(f, Pxx)
plt.title(r'$\theta$ Power Spectrum (log scale y)')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()

plt.close()
f, Pxx = signal.periodogram(time_series, 1./timestep, scaling='spectrum')
plt.plot(f, Pxx, '-')
plt.title(r'$\theta$ Power Spectrum')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()

plt.close()
f, Pxx_den = signal.periodogram(time_series, 1./timestep, scaling='density')
plt.semilogy(f, Pxx_den)
plt.title(r'$\theta$ Power Spectrum Density (log scale y)')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()

plt.close()
f, Pxx_den = signal.periodogram(time_series, 1./timestep, scaling='density')
plt.plot(f, Pxx_den, '-')
plt.title(r'$\theta$ Power Spectrum Density')
plt.xlabel('frequency [Hz]')
plt.ylabel('[(time_series_units)**2]')
plt.grid()
plt.show()
```

```python
print('Frecuencies length = {} \nSpectrum length = {}\n'.format(len(f), len(Pxx)))

Pxx_max_index = np.argmax(Pxx)
f_max = f[Pxx_max_index]
print('Maximum frecuency = {}\nCorresponding period = {}\n'.format(f_max, 1./f_max))

f2 = np.hstack((f[0:Pxx_max_index], f[Pxx_max_index+1:]))
Pxx2 = np.hstack((Pxx[0:Pxx_max_index], Pxx[Pxx_max_index+1:]))
Pxx2_max_index = np.argmax(Pxx2)
f2_max = f2[Pxx2_max_index]
print('Second maximum frecuency = {}\nCorresponding period = {}\n'.format(f2_max, 1./f2_max))

f3 = np.hstack((f2[0:Pxx2_max_index], f2[Pxx2_max_index+1:]))
Pxx3 = np.hstack((Pxx2[0:Pxx2_max_index], Pxx2[Pxx2_max_index+1:]))
Pxx3_max_index = np.argmax(Pxx3)
f3_max = f3[Pxx3_max_index]
print('Third maximum frecuency = {}\nCorresponding period = {}\n'.format(f3_max, 1./f3_max))

f4 = np.hstack((f3[0:Pxx3_max_index], f3[Pxx3_max_index+1:]))
Pxx4 = np.hstack((Pxx3[0:Pxx3_max_index], Pxx3[Pxx3_max_index+1:]))
Pxx4_max_index = np.argmax(Pxx4)
f4_max = f4[Pxx4_max_index]
print('Fourth maximum frecuency = {}\nCorresponding period = {}\n'.format(f4_max, 1./f4_max))
```

    Frecuencies length = 501
    Spectrum length = 501

    Maximum frecuency = 0.3996
    Corresponding period = 2.5025025025025025

    Second maximum frecuency = 0.43956
    Corresponding period = 2.275002275002275

    Third maximum frecuency = 0.35964
    Corresponding period = 2.7805583361138915

    Fourth maximum frecuency = 0.47952000000000006
    Corresponding period = 2.0854187520854186

### Disccussion

It seems we have only found periodic regime. If we still trying out with different values for **ALL** the parameters, it will require a huge effort. So we need an smarter method to perform it, and the very best way is first to nondimensionalize the system.

### Dimensionless system

The system an be rewritten as:

$$\frac{\mathrm{d}^{2}x}{\mathrm{d}\tau^{2}} = x \; \left( \frac{\mathrm{d}\theta}{\mathrm{d}\tau} \right)^{2} + \cos{\theta} - \gamma \left( 1 - x \right) $$

$$\frac{\mathrm{d}^{2} \theta}{\mathrm{d}\tau^{2}} = - \frac{\sin{\theta}}{x} - \frac{2}{x} \frac{\mathrm{d} x}{\mathrm{d} \tau} \frac{\mathrm{d} \theta}{\mathrm{d} \tau} $$

Let:

$$ v_{ad} = \frac{\mathrm{d}x}{\mathrm{d} \tau} $$

$$ \omega = \frac{\mathrm{d}\theta}{\mathrm{d} \tau} $$

So:

$$ \frac{\mathrm{d}x}{\mathrm{d} \tau} = F_{1} (x, \theta, v_{ad}, \omega, \gamma) = v_{ad} $$

$$ \frac{\mathrm{d}\theta}{\mathrm{d} \tau} = F_{2} (x, \theta, v_{ad}, \omega, \gamma) = \omega$$

$$ \frac{\mathrm{d}v_{ad}}{\mathrm{d} \tau} = F_{3} (x, \theta, v_{ad}, \omega, \gamma) =  x \; \omega^{2} + \cos{\theta} + \gamma \left( 1 - x \right) $$

$$ \frac{\mathrm{d}\omega}{\mathrm{d} \tau} = F_{4} (x, \theta, v_{ad}, \omega, \gamma) = - \frac{\sin{\theta}}{x} - \frac{2 \; v_{ad} \; \omega}{x}  $$

Now we are working in space $( \; x \; , \; \theta \; , \; v_{ad} \; , \; \omega \; )$ and the only param is $\gamma$

In order to plot real motion of the mass, take into account that:

$$ x_{real} = L_{0} \; x \; \sin{\theta}$$

$$ y_{real} = - L_{0} \; x \; \cos{\theta}$$

The Jacobian of the system is:

$ J = \begin{pmatrix}
  0 & 0 & 1 & 0 \\
  0 & 0 & 0 & 1 \\
  \omega^{2} - \gamma & -\sin{\theta} & 0 & 2 x \\
  \frac{1}{x^{2}} \left( \sin{\theta} + 2 \; v_{ad} \; \omega \right)
  & - \frac{\cos{\theta}}{x} & - \frac{2 \omega}{x} & - \frac{2 \; v_{ad}}{x}
 \end{pmatrix} $

  $ J_{1} = \begin{pmatrix}
  0 & 0 & 1 & 0 \\
  0 & 0 & 0 & 1 \\
  -\gamma & 0 & 0 & 2 \frac{\gamma + 1}{\gamma} \\
  0 & - \frac{1}{x} & 0 & 0
 \end{pmatrix} $

  $ J_{1} = \begin{pmatrix}
  0 & 0 & 1 & 0 \\
  0 & 0 & 0 & 1 \\
  -\gamma & 0 & 0 & 2 \frac{\gamma - 1}{\gamma} \\
  0 & \frac{1}{x} & 0 & 0
 \end{pmatrix} $

```python
from sympy.solvers import solve, nonlinsolve
from sympy import Symbol, symbols
from sympy import sin, cos, limit
from mpmath import *
```

Here we search for a solution of equation $x^{2} == 1$ such that x is in the interval $[0.5, 3]$.

```python
x = Symbol('x')
eq = x**2 - 1
solve([x >= 0.5, x <= 3, eq ], x)
```

    Eq(x, 1.00000000000000)

```python
x, theta, gamma = symbols('x, theta, gamma', real=True)

eq1 = sin(theta)/x

solve(eq1, [x, theta])
```

    [{theta: 0}, {theta: pi}]

```python
# theta = 0 --> cos(0) = 1
eq2 = 1 + gamma * (1 - x)

# theta = pi --> cos(pi) = -1
eq3 = -1 + gamma * (1 - x)

print(solve(eq2, [x, gamma]))
print('\n')
print(solve(eq3, [x, gamma]))
```

    [{x: (gamma + 1)/gamma}]

    [{x: (gamma - 1)/gamma}]

```python
# Functions for solving differential equations and to define fluxes in phase portrait

def F1(x, theta, Vad, omega, gamma):
    return Vad

def F2(x, theta, Vad, omega, gamma):
    return omega

def F3(x, theta, Vad, omega, gamma):
    return x * omega**2 + np.cos(theta) + gamma * (1 - x)

def F4(x, theta, Vad, omega, gamma):
    return - ( np.sin(theta) + 2.0 * Vad * omega) / x

def dYdtau(y, tau, gamma):

    x, theta, Vad, omega = y

    dx = F1(x, theta, Vad, omega, gamma)
    dtheta = F2(x, theta, Vad, omega, gamma)
    dVad = F3(x, theta, Vad, omega, gamma)
    domega = F4(x, theta, Vad, omega, gamma)

    return [dx, dtheta, dVad, domega]
```

```python
# Constant parameters
gamma = 0.5
Lo = 1

# Initial conditions
x0 = Lo
Vad0 = 0.
theta0 =  0.3
omega0 =  0.

y0 = [x0, theta0, Vad0, omega0]

# Time
tau_i = 0
tau_f = 100
Ntau = 4000

tau, tau_step = np.linspace(tau_i, tau_f , Ntau, retstep=True)

print('tau time step = {}'.format(tau_step))

# Solve differential equations
sol = odeint(dYdtau, y0, tau, args=(gamma,))

# Retrieve variales information
x = sol[:, 0]
theta = sol[:, 1]
Vad = sol[:, 2]
omega = sol[:, 3]

# Retrieve (x,y) positions
xReal = Lo * x * np.sin(theta)
yReal = - Lo * x * np.cos(theta)

# Plot positions

plt.close()
plt.figure(figsize=(8,8))
plt.plot(xReal, yReal,"-")
plt.xlabel('y')
plt.ylabel('x')
plt.title('Real motion')
plt.axis([-np.abs(xReal).max(), np.abs(xReal).max(), -np.abs(yReal).max(), 0]) # plt.axis([xmin, xmax, ymin, ymax])
plt.grid()
plt.show()
```

```python
# Constant parameters
gamma = 10.
Lo = 1

# Initial conditions
x0 = Lo
Vad0 = 2.
theta0 =  0.0
omega0 =  2.

y0 = [x0, theta0, Vad0, omega0]

# Time
tau_i = 0
tau_f = 200
Ntau = 8000

tau, tau_step = np.linspace(tau_i, tau_f , Ntau, retstep=True)

print('tau time step = {}'.format(tau_step))

# Solve differential equations
sol = odeint(dYdtau, y0, tau, args=(gamma,))

# Retrieve variales information
x = sol[:, 0]
theta = sol[:, 1]
Vad = sol[:, 2]
omega = sol[:, 3]

# Retrieve (x,y) positions
xReal = Lo * x * np.sin(theta)
yReal = - Lo * x * np.cos(theta)

# Plot positions

plt.close()
plt.figure(figsize=(8,8))
plt.plot(xReal, yReal,"-")
plt.xlabel('y')
plt.ylabel('x')
plt.title('Real motion')
#plt.axis([-np.abs(xReal).max(), np.abs(xReal).max(), -np.abs(yReal).max(), 0]) # plt.axis([xmin, xmax, ymin, ymax])
plt.grid()
plt.show()

# Plot time series

plt.close()
plt.title(r'$x$ time series')
plt.plot(tau, x,"-")
plt.xlabel(r'$\tau$')
plt.ylabel(r'$L$')
plt.grid()
plt.show()

plt.close()
plt.title(r'$\theta$ time series')
plt.plot(tau, theta,"-")
plt.xlabel(r'$\tau$')
plt.ylabel(r'$\theta$')
plt.grid()
plt.show()
```

```python
# Constant parameters
gamma = np.pi
Lo = 1.

# Initial conditions
x0 = Lo
Vad0 = 10.
theta0 =  0.1
omega0 =  20*np.pi

y0 = [x0, theta0, Vad0, omega0]

# Time
tau_i = 0
tau_f = 200
Ntau = 8000

tau, tau_step = np.linspace(tau_i, tau_f , Ntau, retstep=True)

print('tau time step = {}'.format(tau_step))

# Solve differential equations
sol = odeint(dYdtau, y0, tau, args=(gamma,))

# Retrieve variales information
x = sol[:, 0]
theta = sol[:, 1]
Vad = sol[:, 2]
omega = sol[:, 3]

# Retrieve (x,y) positions
xReal = Lo * x * np.sin(theta)
yReal = - Lo * x * np.cos(theta)

# Plot positions

plt.close()
plt.figure(figsize=(8,8))
plt.plot(xReal, yReal,"-")
plt.xlabel('y')
plt.ylabel('x')
plt.title('Real motion')
#plt.axis([-np.abs(xReal).max(), np.abs(xReal).max(), -np.abs(yReal).max(), 0]) # plt.axis([xmin, xmax, ymin, ymax])
plt.grid()
plt.show()

# Plot time series

plt.close()
plt.title(r'$x$ time series')
plt.plot(tau, x,"-")
plt.xlabel(r'$\tau$')
plt.ylabel(r'$L$')
plt.grid()
plt.show()

plt.close()
plt.title(r'$\theta$ time series')
plt.plot(tau, theta,"-")
plt.xlabel(r'$\tau$')
plt.ylabel(r'$\theta$')
plt.grid()
plt.show()
```

```python

```


# Unit Testing `GiRaFFE_NRPy`: $A_k$ to $B^i$

### Author: Patrick Nelson

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.

**Module Status:**  In-Progress

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

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.

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

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 = "Unit_Test_GiRaFFE_NRPy_Ccode_library_A2B"
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"],10.0)
gammadet = gri.register_gridfunctions("AUXEVOL","gammadet")

DIM = 3
par.set_parval_from_str("grid::DIM",DIM)

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

```

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(DIM):
    for j in range(DIM):
        for k in range(DIM):
            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(DIM):
    B_analyticU[i] = 0
    for j in range(DIM):
        for k in range(DIM):
            B_analyticU[i] += LeviCivitaUUU[i][j][k] * sp.diff(AD[k],rfm.xxCart[j])

```

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
out_string = """
// 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
const int MAXFACE = -1;
const int NUL     = +0;
const int MINFACE = +1;
const int NGHOSTS = 1;

// Standard NRPy+ memory access:
#define IDX4(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS[0] * ( (j) + Nxx_plus_2NGHOSTS[1] * ( (k) + Nxx_plus_2NGHOSTS[2] * (g) ) ) )
#define IDX3(i,j,k) ( (i) + Nxx_plus_2NGHOSTS[0] * ( (j) + Nxx_plus_2NGHOSTS[1] * (k) ) )
// Assuming idx = IDX3(i,j,k). Much faster if idx can be reused over and over:
#define IDX4pt(g,idx)   ( (idx) + (Nxx_plus_2NGHOSTS[0]*Nxx_plus_2NGHOSTS[1]*Nxx_plus_2NGHOSTS[2]) * (g) )

"""
```

We'll now define the gridfunction names.

```python
out_string += """
// 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

"""
```

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
out_string += """
#include "../A2B/driver_AtoB.c" // This file contains both functions we need.

"""

cmd.mkdir(os.path.join("A2B/"))
shutil.copy(os.path.join("../GiRaFFE_HO/GiRaFFE_Ccode_library/A2B/driver_AtoB.c"),os.path.join("A2B/"))

```

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
out_string += """
REAL a,b,c,d,e,f,g,h,l,m,n,o,p,q,r,s,t,u;

void calculate_exact_BU(const int Nxx_plus_2NGHOSTS[3],REAL *xx[3],double *auxevol_gfs) {
    for(int i2=0;i2<Nxx_plus_2NGHOSTS[2];i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS[1];i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS[0];i0++) {
        REAL xx0 = xx[0][i0];
        REAL xx1 = xx[1][i1];
        REAL xx2 = xx[2][i2];
"""

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]),\
                          ]
B_analyticU_kernel = fin.FD_outputC("returnstring",B_analyticU_to_print,params="outCverbose=False")
out_string += B_analyticU_kernel

out_string += """
    }
}

"""

gri.glb_gridfcs_list = []
import GiRaFFE_HO.GiRaFFE_HO_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_HO_A2B("A2B/")

```

    Wrote to file "A2B/B_from_A_order10.h"
    Wrote to file "A2B/B_from_A_order8.h"
    Wrote to file "A2B/B_from_A_order6.h"
    Wrote to file "A2B/B_from_A_order4.h"
    Wrote to file "A2B/B_from_A_order2.h"
    Wrote to file "A2B/B_from_A_order2_dirx0_dnwind.h"
    Wrote to file "A2B/B_from_A_order2_dirx0_upwind.h"
    Wrote to file "A2B/B_from_A_order2_dirx1_dnwind.h"
    Wrote to file "A2B/B_from_A_order2_dirx1_upwind.h"
    Wrote to file "A2B/B_from_A_order2_dirx2_dnwind.h"
    Wrote to file "A2B/B_from_A_order2_dirx2_upwind.h"

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
out_string += """
void calculate_AD(const int Nxx_plus_2NGHOSTS[3],REAL *xx[3],double *out_gfs) {
    for(int i2=0;i2<Nxx_plus_2NGHOSTS[2];i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS[1];i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS[0];i0++) {
        REAL xx0 = xx[0][i0];
        REAL xx1 = xx[1][i1];
        REAL xx2 = xx[2][i2];
"""

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]),\
              ]
AD_kernel = fin.FD_outputC("returnstring",AD_to_print,params="outCverbose=False")
out_string += AD_kernel

out_string += """
    }
}
"""
```

We will define the extent of our grid here.

```python
out_string += """
const REAL xmin = -0.01,xmax=0.01;
const REAL ymin = -0.01,ymax=0.01;
const REAL zmin = -0.01,zmax=0.01;

"""
```

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
out_string += """
main(int argc, const char *argv[]) {
    // Let the first argument be the test we're doing. 1 = coarser grid, 0 = finer grid.
    int do_quadratic_test = atoi(argv[4]);
    // We'll use this grid. It has one point and one ghost zone.
    const int Nxx[3] = {atoi(argv[1]),atoi(argv[2]),atoi(argv[3])};
    int Nxx_plus_2NGHOSTS[3];
    for (int i=0;i<3;i++) Nxx_plus_2NGHOSTS[i] = Nxx[i]+2*NGHOSTS;

    const REAL xxmin[3] = {xmin,ymin,zmin};
    const REAL xxmax[3] = {xmax,ymax,zmax};

    REAL dxx[3];
    for(int i=0;i<3;i++) dxx[i] = (xxmax[i] - xxmin[i]) / ((REAL)Nxx[i]+1.0);

    // 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];
    for(int i=0;i<3;i++) {
        xx[i] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS[i]);
        for(int j=0;j<Nxx_plus_2NGHOSTS[i];j++) {
            xx[i][j] = xxmin[i] + ((REAL)(j))*dxx[i]; // Face-centered grid.
        }
    }
    //for(int i=0;i<Nxx_plus_2NGHOSTS[0];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_2NGHOSTS[2] * Nxx_plus_2NGHOSTS[1] * Nxx_plus_2NGHOSTS[0]);
    REAL *evol_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS[2] * Nxx_plus_2NGHOSTS[1] * Nxx_plus_2NGHOSTS[0]);

    for(int i2=0;i2<Nxx_plus_2NGHOSTS[2];i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS[1];i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS[0];i0++) {
        //auxevol_gfs[IDX4(GAMMADETGF,i0,i1,i2)] = 1.0; // Flat Space
        auxevol_gfs[IDX4(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.
    // 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) {
        calculate_AD(Nxx_plus_2NGHOSTS,xx,evol_gfs);

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

        // And now for the numerical derivatives:
        driver_A_to_B(Nxx,Nxx_plus_2NGHOSTS,dxx,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");
        //Two variables for inside the loop:
        int ghost_zone_overlap;int indices[3];
        for(int i2=0;i2<Nxx_plus_2NGHOSTS[2];i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS[1];i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS[0];i0++) {
            // Are we on an edge/vertex? This algorithm can probably be improved.
            ghost_zone_overlap = 0;
            indices[0] = i0;
            indices[1] = i1;
            indices[2] = i2;
            for(int dim=0;dim<3;dim++) {
                if(indices[dim]%(Nxx[dim]+NGHOSTS)<NGHOSTS) {
                    ghost_zone_overlap++;
                }
            }
            if (ghost_zone_overlap < 2) {
                // Don't print if we're on an edge or vertex
                printf("SDA: %.3f, %.3f, %.3f\\n",
                       1.0-log10(2.0*fabs(auxevol_gfs[IDX4(B_ANALYTICU0GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU0GF,i0,i1,i2)])/(fabs(auxevol_gfs[IDX4(B_ANALYTICU0GF,i0,i1,i2)])+fabs(auxevol_gfs[IDX4(BU0GF,i0,i1,i2)])+1.e-15)),
                       1.0-log10(2.0*fabs(auxevol_gfs[IDX4(B_ANALYTICU1GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU1GF,i0,i1,i2)])/(fabs(auxevol_gfs[IDX4(B_ANALYTICU1GF,i0,i1,i2)])+fabs(auxevol_gfs[IDX4(BU1GF,i0,i1,i2)])+1.e-15)),
                       1.0-log10(2.0*fabs(auxevol_gfs[IDX4(B_ANALYTICU2GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU2GF,i0,i1,i2)])/(fabs(auxevol_gfs[IDX4(B_ANALYTICU2GF,i0,i1,i2)])+fabs(auxevol_gfs[IDX4(BU2GF,i0,i1,i2)])+1.e-15))
                       );
            }
        }
    }

    // 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(Nxx_plus_2NGHOSTS,xx,evol_gfs);

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

    // And now for the numerical derivatives:
    driver_A_to_B(Nxx,Nxx_plus_2NGHOSTS,dxx,evol_gfs,auxevol_gfs);

    // Some variables needed for the loop:
    int ghost_zone_overlap; int indices[3];

    char filename[100];
    sprintf(filename,"out%d-numer.txt",Nxx[0]);
    FILE *out2D = fopen(filename, "w");
    if(do_quadratic_test) {
        for(int i2=0;i2<Nxx_plus_2NGHOSTS[2];i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS[1];i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS[0];i0++) {
            ghost_zone_overlap = 0;
            indices[0] = i0;
            indices[1] = i1;
            indices[2] = i2;
            for(int dim=0;dim<3;dim++) {
                if(indices[dim]%(Nxx[dim]+NGHOSTS)<NGHOSTS) {
                    ghost_zone_overlap++;
                }
            }
            if (ghost_zone_overlap < 2) {
                // We print the difference between approximate and exact numbers.
                fprintf(out2D,"%.16e\t%.16e\t%.16e\\n",
                        auxevol_gfs[IDX4(B_ANALYTICU0GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU0GF,i0,i1,i2)],
                        auxevol_gfs[IDX4(B_ANALYTICU1GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU1GF,i0,i1,i2)],
                        auxevol_gfs[IDX4(B_ANALYTICU2GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU2GF,i0,i1,i2)]
                        );
            }
        }
    }
    else {
        for(int i2=0;i2<Nxx_plus_2NGHOSTS[2];i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS[1];i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS[0];i0++) {
            ghost_zone_overlap = 0;
            indices[0] = i0;
            indices[1] = i1;
            indices[2] = i2;
            for(int dim=0;dim<3;dim++) {
                if(indices[dim]%(Nxx[dim]+NGHOSTS)<NGHOSTS) {
                    ghost_zone_overlap++;
                }
            }
            // Don't print on the edges or corners
            if (ghost_zone_overlap < 2) {
                // Only print points shared between the grids
                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\\n",
                            auxevol_gfs[IDX4(B_ANALYTICU0GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU0GF,i0,i1,i2)],
                            auxevol_gfs[IDX4(B_ANALYTICU1GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU1GF,i0,i1,i2)],
                            auxevol_gfs[IDX4(B_ANALYTICU2GF,i0,i1,i2)]-auxevol_gfs[IDX4(BU2GF,i0,i1,i2)]
                            );
                }
            }
        }

    }
    fclose(out2D);
}
"""

```

Now, we must write out the code to a `.C` file.

```python
with open(os.path.join(out_dir,"A2B_unit_test.C"),"w") as file:
    file.write(out_string)
```

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")

# os.chdir(out_dir)
print("Now running...\n")
start = time.time()
# cmd.Execute(os.path.join("Stilde_flux_unit_test"))
!./Validation/A2B_unit_test 1 1 1 1
# To do a convergence test, we'll also need a second grid with twice the resolution.
!./Validation/A2B_unit_test 3 3 3 0
end = time.time()
print("Finished in "+str(end-start)+" seconds.\n\n")
# os.chdir(os.path.join("../"))

```

    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 1.22558999062 seconds.
    Finished compilation.
    Finished in 1.23869585991 seconds.

    Now running...

    This test uses quadratic vector potentials, so the magnetic fields should agree to roundoff error.
    Below, each row represents one point. Each column represents a component of the magnetic field.
    Shown is the number of Significant Digits of Agreement, at least 13 is good, higher is better:

    SDA: 13.201, 13.958, inf
    SDA: 13.958, inf, 13.684
    SDA: inf, 13.958, 13.256
    SDA: inf, inf, inf
    SDA: inf, 13.958, 13.256
    SDA: 13.958, inf, 13.684
    SDA: 13.201, 13.958, inf
    Finished in 0.0554201602936 seconds.

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$.

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

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

convergence = np.log(np.divide(np.abs(Data1),np.abs(Data2)))/np.log(2)
print("Convergence test: All should be approximately 2\n")
print(convergence)
```

    Convergence test: All should be approximately 2

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