# FITC

```python
import sys
# Add the symgp folder path to the sys.path list
module_path = r'/Users/jaduol/Documents/Uni (original)/Part II/IIB/MEng Project/'
if module_path not in sys.path:
    sys.path.append(module_path)

from symgp import SuperMatSymbol, utils, MVG, Variable, SuperDiagMat, Kernel
from sympy import symbols, ZeroMatrix, Identity
from IPython.display import display, Math, Latex
```

```python
import imp
imp.reload(utils)
imp.reload(kernel)
```

```python
m, n, l = symbols('m n l')
s_y = symbols('\u03c3_y')
K = Kernel()
```

## 1. Induding prior

```python
u = Variable('u',m,1)
p_u = MVG([u],mean=ZeroMatrix(m,1),cov=K(u,u))

print("p_u:")
display(Latex(utils.matLatex(p_u)))
```

    p_u:

\begin{align*}
p\left(\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{u};\mathbf{m}_{\mathbf{u}},\mathbf{\Sigma}_{\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{u}} &= \mathbf{0}\\
\mathbf{\Sigma}_{\mathbf{u}} &= \mathbf{K}_{\mathbf{u},\mathbf{u}}\\
\end{align*}

## 2. Training and Test Conditionals

```python
f, fs, y = utils.variables("f f_{*} y",[n, l, n])

q_fgu = MVG([f], mean=K(f,u)*K(u,u).I*u,
                 cov=SuperDiagMat(K(f,f)-K(f,u)*K(u,u).I*K(u,f)),
                 cond_vars=[u],
                 prefix='q_{FITC}')

print("q_fgu:")
display(Latex(utils.matLatex(q_fgu)))
```

    q_fgu:

\begin{align*}
q_{FITC}\left(\mathbf{f}|\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{f};\mathbf{m}_{\mathbf{f}|\mathbf{u}},\mathbf{\Sigma}_{\mathbf{f}|\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{f}|\mathbf{u}} &= \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\\
\mathbf{\Sigma}_{\mathbf{f}|\mathbf{u}} &= \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}]\\
\end{align*}

```python
q_fsgu = MVG([fs], mean=K(fs,u)*K(u,u).I*u,
                   cov=K(fs,fs)-K(fs,u)*K(u,u).I*K(u,fs),
                   cond_vars=[u],
                   prefix='q_{FITC}')

print("q_fgu:")
display(Latex(utils.matLatex(q_fsgu)))
```

    q_fgu:

\begin{align*}
q_{FITC}\left(\mathbf{f_{*}}|\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{f_{*}};\mathbf{m}_{\mathbf{f_{*}}|\mathbf{u}},\mathbf{\Sigma}_{\mathbf{f_{*}}|\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{f_{*}}|\mathbf{u}} &= \mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\\
\mathbf{\Sigma}_{\mathbf{f_{*}}|\mathbf{u}} &= \mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}} - \mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}\\
\end{align*}

```python
# q(f,fs|u)
q_f_fs_g_u = q_fgu*q_fsgu

print("q_f_fs_g_u:")
display(Latex(utils.matLatex(q_f_fs_g_u)))
```

    q_f_fs_g_u:

\begin{align*}
q_{FITC}\left(\mathbf{f},\mathbf{f_{*}}|\mathbf{u}\right)&= \mathcal{N}\left(\left[\begin{smallmatrix}\mathbf{f}\\\mathbf{f_{*}}\end{smallmatrix}\right];\mathbf{m}_{\mathbf{f},\mathbf{f_{*}}|\mathbf{u}},\mathbf{\Sigma}_{\mathbf{f},\mathbf{f_{*}}|\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{f},\mathbf{f_{*}}|\mathbf{u}} &= \left[\begin{smallmatrix}\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\\\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{f},\mathbf{f_{*}}|\mathbf{u}} &= \left[\begin{smallmatrix}\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}]&\mathbf{0}\\\mathbf{0}&\mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}} - \mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}\end{smallmatrix}\right]\\
\end{align*}

```python
# q(f,fs,u)
q_f_fs_u = q_f_fs_g_u*p_u

print("q_f_fs_u:")
display(Latex(utils.matLatex(q_f_fs_u)))
```

    q_f_fs_u:

\begin{align*}
q_{FITC}\left(\mathbf{f},\mathbf{f_{*}},\mathbf{u}\right)&= \mathcal{N}\left(\left[\begin{smallmatrix}\mathbf{f}\\\mathbf{f_{*}}\\\mathbf{u}\end{smallmatrix}\right];\mathbf{m}_{\mathbf{f},\mathbf{f_{*}},\mathbf{u}},\mathbf{\Sigma}_{\mathbf{f},\mathbf{f_{*}},\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{f},\mathbf{f_{*}},\mathbf{u}} &= \left[\begin{smallmatrix}\mathbf{0}\\\mathbf{0}\\\mathbf{0}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{f},\mathbf{f_{*}},\mathbf{u}} &= \left[\begin{smallmatrix}\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}&\mathbf{K}_{\mathbf{f},\mathbf{u}}\\\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}}&\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}}\\\mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}&\mathbf{K}_{\mathbf{u},\mathbf{u}}\end{smallmatrix}\right]\\
\end{align*}

```python
# Effective prior: q(f,fs)
q_f_fs = q_f_fs_u.marginalise([u])

print("q_f_fs:")
display(Latex(utils.matLatex(q_f_fs)))
```

    q_f_fs:

\begin{align*}
q_{FITC}\left(\mathbf{f},\mathbf{f_{*}}\right)&= \mathcal{N}\left(\left[\begin{smallmatrix}\mathbf{f}\\\mathbf{f_{*}}\end{smallmatrix}\right];\mathbf{m}_{\mathbf{f},\mathbf{f_{*}}},\mathbf{\Sigma}_{\mathbf{f},\mathbf{f_{*}}}\right)\\
\mathbf{m}_{\mathbf{f},\mathbf{f_{*}}} &= \left[\begin{smallmatrix}\mathbf{0}\\\mathbf{0}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{f},\mathbf{f_{*}}} &= \left[\begin{smallmatrix}\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}\\\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}}\end{smallmatrix}\right]\\
\end{align*}

## 3. Likelihood

```python
p_ygf = MVG([y],mean=f,cov=s_y**2*Identity(n),cond_vars=[f])

print("p_ygf:")
display(Latex(utils.matLatex(p_ygf)))
```

    p_ygf:

\begin{align*}
p\left(\mathbf{y}|\mathbf{f}\right)&= \mathcal{N}\left(\mathbf{y};\mathbf{m}_{\mathbf{y}|\mathbf{f}},\mathbf{\Sigma}_{\mathbf{y}|\mathbf{f}}\right)\\
\mathbf{m}_{\mathbf{y}|\mathbf{f}} &= \mathbf{f}\\
\mathbf{\Sigma}_{\mathbf{y}|\mathbf{f}} &= \sigma_y^{2} \mathbf{I}\\
\end{align*}

## 4. Inference

```python
# q(f,fs,y)
q_f_fs_y = p_ygf*q_f_fs

print("q_f_fs_y:")
display(Latex(utils.matLatex(q_f_fs_y)))
```

    q_f_fs_y:

\begin{align*}
q_{FITC}\left(\mathbf{y},\mathbf{f_{*}},\mathbf{f}\right)&= \mathcal{N}\left(\left[\begin{smallmatrix}\mathbf{y}\\\mathbf{f_{*}}\\\mathbf{f}\end{smallmatrix}\right];\mathbf{m}_{\mathbf{y},\mathbf{f_{*}},\mathbf{f}},\mathbf{\Sigma}_{\mathbf{y},\mathbf{f_{*}},\mathbf{f}}\right)\\
\mathbf{m}_{\mathbf{y},\mathbf{f_{*}},\mathbf{f}} &= \left[\begin{smallmatrix}\mathbf{0}\\\mathbf{0}\\\mathbf{0}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{y},\mathbf{f_{*}},\mathbf{f}} &= \left[\begin{smallmatrix}\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}&\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\\\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}}&\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\\\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}&\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\end{smallmatrix}\right]\\
\end{align*}

```python
# q(f,fs|y)
q_f_fs_g_y = q_f_fs_y.condition([y])

print("q_f_fs_g_y:")
display(Latex(utils.matLatex(q_f_fs_g_y)))
```

    q_f_fs_g_y:

\begin{align*}
q_{FITC}\left(\mathbf{f_{*}},\mathbf{f}|\mathbf{y}\right)&= \mathcal{N}\left(\left[\begin{smallmatrix}\mathbf{f_{*}}\\\mathbf{f}\end{smallmatrix}\right];\mathbf{m}_{\mathbf{f_{*}},\mathbf{f}|\mathbf{y}},\mathbf{\Sigma}_{\mathbf{f_{*}},\mathbf{f}|\mathbf{y}}\right)\\
\mathbf{m}_{\mathbf{f_{*}},\mathbf{f}|\mathbf{y}} &= \left[\begin{smallmatrix}\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{y}\\\left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right) \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{y}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{f_{*}},\mathbf{f}|\mathbf{y}} &= \left[\begin{smallmatrix}\mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}} - \mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}&\mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} - \mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)\\\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}} - \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right) \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}&\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} - \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right) \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)\end{smallmatrix}\right]\\
\end{align*}

```python
# q(fs|y)
q_fs_g_y = q_f_fs_g_y.marginalise([f])

print("q_fs_g_y:")
display(Latex(utils.matLatex(q_fs_g_y)))
```

    q_fs_g_y:

\begin{align*}
q_{FITC}\left(\mathbf{f_{*}}|\mathbf{y}\right)&= \mathcal{N}\left(\mathbf{f_{*}};\mathbf{m}_{\mathbf{f_{*}}|\mathbf{y}},\mathbf{\Sigma}_{\mathbf{f_{*}}|\mathbf{y}}\right)\\
\mathbf{m}_{\mathbf{f_{*}}|\mathbf{y}} &= \mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{y}\\
\mathbf{\Sigma}_{\mathbf{f_{*}}|\mathbf{y}} &= \mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}} - \mathbf{K}_{\mathbf{f_{*}},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f_{*}}}\\
\end{align*}

## Example of Kernel substitution

```python
import imp
imp.reload(utils)
from symgp import Covariance
M = Covariance(u, name='M_{u,u}', full_expr=K(u,u).I)
Q = Kernel(name='Q', sub_kernels=[K,K], kernel_type='mul', mat=M)
#repl_dict = {K(fs,u)*K(u,u).I*K(u,f): Q(fs,f),
#             K(f,u)*K(u,u).I*K(u,f): Q(f,f),
#             K(f,u)*K(u,u).I*K(u,fs): Q(f,fs)}
repl_dict = [Q]

q_fs_g_y.mean.expanded = utils.replace(q_fs_g_y.mean.to_full_expr(), repl_dict)
q_fs_g_y.covar.expanded = utils.replace(q_fs_g_y.covar.to_full_expr(), repl_dict)

```

    subbed_expr:  Q_{f_{*},f}*(σ_y**2*I + diag[K_{f,f} + (-1)*Q_{f,f}] + Q_{f,f})^-1*y
    srepr(subbed_expr):  SuperMatMul(KernelMatrix('Q_{f_{*},f}', Symbol('l'), Symbol('n')), Inverse(SuperMatAdd(MatMul(Pow(Symbol('σ_y'), Integer(2)), Identity(Symbol('n'))), SuperDiagMat(SuperMatAdd(KernelMatrix('K_{f,f}', Symbol('n'), Symbol('n')), SuperMatMul(Integer(-1), KernelMatrix('Q_{f,f}', Symbol('n'), Symbol('n'))))), SuperMatMul(KernelMatrix('Q_{f,f}', Symbol('n'), Symbol('n'))))), Variable('y', Symbol('n'), Integer(1)))
    subbed_expr:  K_{f_{*},f_{*}} + (-1)*Q_{f_{*},f}*(σ_y**2*I + diag[K_{f,f} + (-1)*Q_{f,f}] + Q_{f,f})^-1*Q_{f,f_{*}}
    srepr(subbed_expr):  SuperMatAdd(KernelMatrix('K_{f_{*},f_{*}}', Symbol('l'), Symbol('l')), SuperMatMul(Integer(-1), KernelMatrix('Q_{f_{*},f}', Symbol('l'), Symbol('n')), Inverse(SuperMatAdd(MatMul(Pow(Symbol('σ_y'), Integer(2)), Identity(Symbol('n'))), SuperDiagMat(SuperMatAdd(KernelMatrix('K_{f,f}', Symbol('n'), Symbol('n')), SuperMatMul(Integer(-1), KernelMatrix('Q_{f,f}', Symbol('n'), Symbol('n'))))), SuperMatMul(KernelMatrix('Q_{f,f}', Symbol('n'), Symbol('n'))))), KernelMatrix('Q_{f,f_{*}}', Symbol('n'), Symbol('l'))))

```python
imp.reload(utils)

print("q_fs_g_y (replaced):")
display(Latex(utils.matLatex(q_fs_g_y)))
```

    q_fs_g_y (replaced):

\begin{align*}
q_{FITC}\left(\mathbf{f_{*}}|\mathbf{y}\right)&= \mathcal{N}\left(\mathbf{f_{*}};\mathbf{m}_{\mathbf{f_{*}}|\mathbf{y}},\mathbf{\Sigma}_{\mathbf{f_{*}}|\mathbf{y}}\right)\\
\mathbf{m}_{\mathbf{f_{*}}|\mathbf{y}} &= \mathbf{Q}_{\mathbf{f_{*}},\mathbf{f}} \left(\mathbf{Q}_{\mathbf{f},\mathbf{f}} + \sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{Q}_{\mathbf{f},\mathbf{f}}]\right)^{-1} \mathbf{y}\\
\mathbf{\Sigma}_{\mathbf{f_{*}}|\mathbf{y}} &= \mathbf{K}_{\mathbf{f_{*}},\mathbf{f_{*}}} - \mathbf{Q}_{\mathbf{f_{*}},\mathbf{f}} \left(\mathbf{Q}_{\mathbf{f},\mathbf{f}} + \sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{Q}_{\mathbf{f},\mathbf{f}}]\right)^{-1} \mathbf{Q}_{\mathbf{f},\mathbf{f_{*}}}\\
\end{align*}

```python

```


# 【課題】量子相関を調べる

第一回の実習ではCHSH不等式の破れを調べるために、2つの量子ビットの相関関数$C^{i} \, (i=0,1,2,3)$という量を量子コンピュータを使って計算しました。この課題では、この量をもう少し細かく調べてみましょう。

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

$\newcommand{\ket}[1]{|#1\rangle}$
$\newcommand{\bra}[1]{\langle#1|}$

## QCシミュレータの使い方

実習で見たように、QCで実現される量子状態は、量子力学の公理に基づいて理論的に計算・予測できます。そこで用いられる数学的操作も単なる足し算や掛け算（線形演算）なので、実はQCの量子状態は（古典）計算機で比較的簡単に計算できます。当然のことですが、QCは何も魔法のブラックボックスというわけではありません。

ただし、古典計算機で量子状態を再現するためには、特殊な場合を除いて、量子ビット数の指数関数的な量のメモリが必要になります。これも前半で見たように、$n$量子ビットあれば、系の自由度（degrees of freedom / dof: 実数自由パラメータの数）は$2^{n+1} - 2$ですので、例えば各自由度を64ビット（＝8バイト）の浮動小数点で表現するとしたら、必要なメモリは(-2を無視して)

$$
2^3\, \mathrm{(bytes / dof)} \times 2^{n+1}\, \mathrm{(dof)} = 2^{n+4}\, \mathrm{(bytes)}
$$

なので、$n=16$で1 MiB、$n=26$で1 GiB、$n=36$で1 TiBです。現在の計算機では、ハイエンドワークステーションでRAMが$\mathcal{O}(1)$ TiB、スパコン「富岳」で5 PB (~252 bytes)なのに対し、QCではすでに$n=65$のものが存在するので、既に古典計算機でまともにシミュレートできない機械が存在していることになります。

しかし、逆に言うと、$n \sim 30$程度までの回路であれば、ある程度のスペックを持った計算機で厳密にシミュレートできるということが言えます。じっさい世の中には[数多くの](https://quantiki.org/wiki/list-qc-simulators)シミュレータが存在します。Qiskitにも様々な高機能シミュレータが同梱されています。

シミュレーションはローカル（手元のPythonを動かしているコンピュータ）で実行できるので、ジョブを投げて結果を待つ時間が省けます。この課題ではたくさんの細かい量子計算をするので、実機を使わず、`qasm_simulator`というQiskitに含まれるシミュレータを利用します。

Qiskitのシミュレータには`Aer`というオブジェクトからアクセスします。`Aer`は実習で登場した`IBMQ`と同様の構造をしており、複数のシミュレータをバックエンドとして管理しています。そのうちの`qasm_simulator`を取り出します。

```python
# まずは全てインポート
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize, Bounds
from qiskit import QuantumCircuit, Aer, execute
from qiskit.visualization import plot_histogram

print('notebook ready')
```

```python
simulator = Aer.get_backend('qasm_simulator')
print(simulator.name())
```

実習の内容を再現してみましょう。

```python
circuits = []
for ic in range(4):
    circuit = QuantumCircuit(2, name='circuit{}'.format(ic))
    circuit.h(0)
    circuit.cx(0, 1)
    circuits.append(circuit)

circuits[0].ry(-np.pi / 4., 1)
circuits[1].ry(-3. * np.pi / 4., 1)
circuits[2].ry(-np.pi / 4., 1)
circuits[3].ry(-3. * np.pi / 4., 1)

circuits[2].ry(-np.pi / 2., 0)
circuits[3].ry(-np.pi / 2., 0)

for circuit in circuits:
    circuit.measure_all()

shots = 10000

sim_job = execute(circuits, backend=simulator, shots=shots)

sim_result = sim_job.result()

C = []
for circuit in circuits:
    c = sim_result.get_counts(circuit)
    ax = plt.figure().add_subplot()
    plot_histogram(c, ax=ax)

    C.append((c['00'] + c['11'] - c['01'] - c['10']) / shots)

S = C[0] - C[1] + C[2] + C[3]
print('S =', S)
```

上のように、`qasm_simulator`は実機と同様に`execute`関数で実行でき、ヒストグラムデータを返します。実機ではショット数に制限がありますが、シミュレータにはありません。ただしショット数が多いほど、当然実行に時間がかかります。といってもこの程度の回路であれば常識的なショット数ならほぼ瞬間的にジョブの実行が終わるので、上の例では実習で使った`job_monitor()`関数を使用していません。また、シミュレータにはノイズがない[^simulator_noise]ので、$S$の計算結果が統計誤差の範囲内で理論値と一致していることが見て取れます。

[^simulator_noise]: 標準設定において。実機の振る舞いに従うよう、あえてノイズを加えるような設定も存在します。

## 測定基底の変換

さて、おさらいをすると、上の$C^{0,1,2,3}$を計算する4つの回路は以下のようなものでした。

```python
for circuit in circuits:
    ax = plt.figure().add_subplot()
    circuit.draw('mpl', ax=ax)
```

ベル状態を作るところまですべての回路で共通で、その後それぞれ異なる角度の$R_y$ゲートをかけています。実習では深く立ち入りませんでしたが、この$R_y$ゲートはベル状態を$\{\ket{0}, \ket{1}\}$とは異なる基底で測定するために置かれています。どういうことか、以下で説明します。

ここまで「測定」とはレジスタの量子状態$\sum_{j=0}^{2^n-1} c_j \ket{j}$からビット列$j$を確率$|c_j|^2$で得る行為である、と説明してきました。しかし、本来「測定」はもっと一般的な概念です。それを理解するために、まず、量子力学的には計算基底状態$\ket{j}$は何ら特別な状態ではないということを理解しておきましょう。

例えば1量子ビットにおいて、計算基底状態は$\ket{0}$と$\ket{1}$ですが、以下のような状態$\ket{\theta}$と$\ket{\theta + \pi}$を考えます。

```{math}
:label: theta_ket_def
\ket{\theta} := R_y(\theta)\ket{0} = \cos\frac{\theta}{2}\ket{0} + \sin\frac{\theta}{2}\ket{1} \\
\ket{\theta + \pi} := R_y(\theta)\ket{1} = -\sin\frac{\theta}{2}\ket{0} + \cos\frac{\theta}{2}\ket{1}
```

すると、

$$
\ket{0} = \cos\frac{\theta}{2}\ket{\theta} - \sin\frac{\theta}{2}\ket{\theta + \pi} \\
\ket{1} = \sin\frac{\theta}{2}\ket{\theta} + \cos\frac{\theta}{2}\ket{\theta + \pi},
$$

つまり、計算基底状態が$\ket{\theta}$と$\ket{\theta + \pi}$の重ね合わせとして表現できます。量子ビットの任意の状態は$\ket{0}$と$\ket{1}$の重ね合わせで表現できるので、$\ket{\theta}$と$\ket{\theta + \pi}$の重ね合わせでも表現できるということになります。そのようなときは$\ket{\theta}$と$\ket{\theta + \pi}$を基底として状態を表しているわけです。

一般に、量子力学的には、2つの異なる状態$\ket{a} \nsim \ket{b}$を考えれば、それらの重ね合わせで量子ビットの任意の状態が表現できます。そして、$\ket{a}$と$\ket{b}$が直交する[^orthogonal]ときに状態$\ket{\psi}$が

$$
\ket{\psi} = \alpha \ket{a} + \beta \ket{b}
$$

と表現されるならば、「基底$\ket{a}$と$\ket{b}$についての測定」という操作を考えることができます。$\ket{\psi}$に対してそのような測定をすると、状態$\ket{a}$が確率$|\alpha|^2$で、状態$\ket{b}$が確率$|\beta|^2$で得られます。

量子計算においても、アルゴリズムの一部として、計算の結果実現した状態を特定の基底で測定するということが多々あります。ところが、ここで若干問題があります。量子コンピュータは実装上、計算基底でしか測定ができないのです。量子力学の理論的には特別でない$\ket{0}$と$\ket{1}$ですが、量子コンピュータという実態にとっては具体的な対応物があるのです。

そこで、量子計算では、状態を任意の基底で測定することを諦め、反対に状態を変化させてしまいます。例えば、本当は上の$\ket{\theta}$と$\ket{\theta + \pi}$という基底で量子ビットの状態$\ket{\psi} = \alpha \ket{\theta} + \beta \ket{\theta + \pi}$を測定したいとします。しかし計算基底でしか測定ができないので、代わりに$R_y(-\theta)$を$\ket{\psi}$にかけます。すると式{eq}`theta_ket_def`から

$$
R_y(-\theta)\ket{\theta} = \ket{0} \\
R_y(-\theta)\ket{\theta + \pi} = \ket{1}
$$

なので、

$$
R_y(-\theta)\ket{\psi} = \alpha \ket{0} + \beta \ket{1}
$$

が得られます。この$R_y(-\theta)\ket{\psi}$を計算基底で測定した結果は、$\ket{\psi}$を$\ket{\theta}, \ket{\theta + \pi}$基底で測定した結果と等価です。

このように、測定を行いたい基底（ここでは$\ket{\theta}, \ket{\theta + \pi}$）を$\ket{0}, \ket{1}$から得るための変換ゲート（$R_y(\theta)$）の逆変換を測定したい状態にかけることで、計算基底での測定で求める結果を得ることができます。

## 観測量の期待値とその計算法

課題の説明に入る前に、さらに話が込み入ってきますが、量子計算でも多出する（ワークブックでは特に{doc}`vqe`以降）概念である「観測量の期待値」について説明します。

観測量とはそのまま「観測できる量」のことで、量子状態から取り出せる（古典的）情報のこととも言えます。例えば、何かしらの粒子の運動を量子力学的に記述した場合、その粒子の位置や運動量などが観測量です。

ケットで表される量子状態に対して、観測量はケットに作用する「エルミート演算子」で表現されます。細かい定義は参考文献に譲り、ここで必要な最小限のことだけ述べると、エルミート演算子は**対角化可能で、実数の固有値を持つ**という性質を持っています。つまり、$A$が$N$次元の量子状態の空間のエルミート演算子であれば、

$$
A \ket{\phi_j} = a_j \ket{\phi_j}
$$

が成り立つような状態$\ket{\phi_j}$と実数$a_j$の組が$N$個存在し、各$\ket{\phi_j} \, (j=0,\dots,N-1)$は互いに直交します。このとき、$\ket{\phi_j}$を固有ベクトル、$a_j$を固有値と呼びます。そして、この固有値$a_j$が、演算子$A$で表される観測量の値に対応します[^continuous_observables]。

さて、$A$が$n$ビット量子レジスタに対する演算子であるとします。レジスタの状態$\ket{\psi}$が$A$の固有ベクトルで

$$
\ket{\psi} = \sum_{j=0}^{2^n-1} \gamma_j \ket{\phi_j}
$$

と分解されるとき、この状態を固有ベクトル基底$\{\ket{\phi_j}\}$で測定すると、状態$\ket{\phi_j}$が確率$|\gamma_j|^2$で得られます。そして、状態が$\ket{\phi_j}$であれば観測量$A$の値は$a_j$です。

そのような測定を多数回繰り返して$A$の値の期待値を求めることを考えます。期待値の定義は「確率変数のすべての値に確率の重みを付けた加重平均」です。$A$が確率分布$\{|\gamma|^2_j\}$に従って値$\{a_j\}$を取るので、

$$
\bra{\psi} A \ket{\psi} = \sum_{j=0}^{2^n-1} a_j |\gamma_j|^2
$$

となります。ここから、量子コンピュータにおいて観測量の期待値を計算する方法を見出すことができます。具体的には、

1. 観測量をエルミート演算子で表現する
1. 演算子を対角化し、固有値と対応する固有ベクトルを求める
1. 固有ベクトルを基底として、レジスタの状態を測定する
1. 測定から得られた確率分布を重みとして固有値の平均値を取る

です。3の測定の際には、上のセクションで説明した測定基底の変換を利用します。

## CHSH不等式を見直す

実習の中で、

$$
C = P_{00} - P_{01} - P_{10} + P_{11}
$$

という量を計算しました。ここで、$P_{lm}$は2つの量子ビットでそれぞれ$l, m \, (=0,1)$が得られる確率でした。実はこの量$C$は、2ビットレジスタにおけるある観測量の期待値として捉えることができます。

まず、1つの量子ビットに対して、固有値が$\pm 1$であるような観測量$\sigma^{\theta}$を考えます。そのような$\sigma^{\theta}$は無数に存在し、固有ベクトルで区別できます。$\theta$は固有ベクトルを決める何らかのパラメータです[^specifying_eigenvectors]。例えば$\sigma^0$という観測量を、計算基底を固有ベクトルとして、

$$
\sigma^0 \ket{0} = \ket{0} \\
\sigma^0 \ket{1} = -\ket{1}
$$

で定義できる、という具合です。

次に、2つの量子ビットA, Bからなるレジスタを考え、$\sigma^{\kappa}$をAの、$\sigma^{\lambda}$をBの観測量とします。また、それぞれの演算子の固有ベクトルを

$$
\sigma^{\kappa} \ket{\kappa_{\pm}} = \pm \ket{\kappa_{\pm}} \\
\sigma^{\lambda} \ket{\lambda_{\pm}} = \pm \ket{\lambda_{\pm}}
$$

で定義します。これらの固有ベクトルを使って、レジスタの状態$\ket{\psi}$を

$$
\ket{\psi} = c_{++} \ket{\lambda_+}_B \ket{\kappa_+}_A + c_{+-} \ket{\lambda_+}_B \ket{\kappa_-}_A + c_{-+} \ket{\lambda_-}_B \ket{\kappa_+}_A + c_{--} \ket{\lambda_-}_B \ket{\kappa_-}_A
$$

と分解します。ケットはAが右、Bが左になるよう並べました。すると、積$\sigma^{\lambda}_B \sigma^{\kappa}_A$の$\ket{\psi}$に関する期待値は、

$$
\bra{\psi} \sigma^{\lambda}_B \sigma^{\kappa}_A \ket{\psi} = |c_{++}|^2 - |c_{+-}|^2 - |c_{-+}|^2 + |c_{--}|^2
$$

です。

最後に、同じ結果を計算基底での測定で表すために、$\ket{\psi}$に対して基底変換を施します。$\{\ket{\kappa_{\pm}}, \ket{\lambda_{\pm}}\}$が何らかのパラメータ付きゲート$R(\theta)$を通して計算基底と

$$
\ket{\kappa_+} = R(\kappa) \ket{0} \\
\ket{\kappa_-} = R(\kappa) \ket{1} \\
\ket{\lambda_+} = R(\lambda) \ket{0} \\
\ket{\lambda_-} = R(\lambda) \ket{1}
$$

で結びついているなら、状態$\ket{\psi'} = R^{-1}_B(\lambda) R^{-1}_A(\kappa) \ket{\psi}$を計算基底で測定したとき、

$$
P_{00} = |c_{++}|^2 \\
P_{01} = |c_{+-}|^2 \\
P_{10} = |c_{-+}|^2 \\
P_{11} = |c_{--}|^2
$$

が成り立ちます。確認のためはっきりさせておくと、左辺は$\ket{\psi'}$を計算基底で測定し、ビット列00, 01, 10, 11を得る確率です。つまり、最初の$C$は

$$
C = \bra{\psi'} \sigma^0_B \sigma^0_A \ket{\psi'} = \bra{\psi} \sigma^{\lambda}_B \sigma^{\kappa}_A \ket{\psi}
$$

を表していたのでした。

これを踏まえて、CHSH不等式の左辺は結局何を計算していたのか、見直してみましょう。ここでベル状態を

$$
\ket{\Psi} = \frac{1}{\sqrt{2}} \left(\ket{00} + \ket{11}\right)
$$

とおき、第0ビットをA、第1ビットをBとします。$R_y(\pi/4)\ket{0}, R_y(\pi/2)\ket{0}, R_y(3\pi/4)\ket{0}$が固有値$+1$の固有ベクトルとなるような演算子をそれぞれ$\sigma^{\pi/4}, \sigma^{\pi/2}, \sigma^{3\pi/4}$とすると、

$$
S = C^0 - C^1 + C^2 + C^3 = \bra{\Psi} \sigma^{\pi/4}_B \sigma^0_A \ket{\Psi} - \bra{\Psi} \sigma^{3\pi/4}_B \sigma^0_A \ket{\Psi} + \bra{\Psi} \sigma^{\pi/4}_B \sigma^{\pi/2}_A \ket{\Psi} + \bra{\Psi} \sigma^{3\pi/4}_B \sigma^{\pi/2}_A \ket{\Psi}
$$

がわかります。

観測量$\sigma^{\theta}$を用いてCHSH不等式をより正確に表現すると、

> 4つのパラメータ$\kappa, \lambda, \mu, \nu$を用いて
> $S(\kappa, \lambda, \mu, \nu) = \langle \sigma^{\kappa}\sigma^{\lambda} \rangle - \langle \sigma^{\kappa}\sigma^{\nu} \rangle + \langle \sigma^{\mu}\sigma^{\lambda} \rangle + \langle \sigma^{\mu}\sigma^{\nu} \rangle$
> という量を定義すると、エンタングルメントのない古典力学において$|S| \leq 2$である

となります。

実習で用いた$\sigma^{\theta}$のパラメータは、$|S|$の値を最大化するものでした。次のセルで数値的にこのことを確かめています。

```python tags=["remove-output"]
def quantum_S(x):
    """
    Three-parameter function to be minimized. Returns -|S| = -|<sigma^k sigma^l> - <sigma^k sigma^n> + <sigma^m sigma^l> + <sigma^m sigma^n>| with k fixed to 0.
    """

    return -np.abs(np.cos(-x[0]) - np.cos(-x[2]) + np.cos(x[1] - x[0]) + np.cos(x[1] - x[2]))

# Initial values
x0 = np.array([np.pi / 2., 0., np.pi / 2.])
# Bounds on all parameters ([0, pi])
bounds = Bounds([0.] * 3, [np.pi] * 3)
# Minimize using scipy.optimize.minimize
res = minimize(quantum_S, x0, method='trust-constr', bounds=bounds)
print('argmax |S|: kappa=0, lambda={:.2f}pi, mu={:.2f}pi, nu={:.2f}pi'.format(*(res.x / np.pi)))
```

[^orthogonal]: ここまで状態ケットの内積に関する説明をしていませんが、線形代数に慣れている方は「量子力学の状態ケットはヒルベルト空間の要素である」と理解してください。慣れていない方は、量子ビットの直行する状態とは$\ket{0}$と$\ket{1}$に同じゲートをかけて得られる状態のことを言うと覚えておいてください。
[^continuous_observables]: 上で粒子の位置や運動量という例を挙げたので、$a_j \, (j=0,\dots,N-1)$と離散的な固有値が観測量の値だというと混乱するかもしれません。位置や運動量といった連続的な値を持つ観測量は、無数に固有値を持つエルミート演算子で表されます。そのような演算子のかかる状態空間は無限次元です。
[^specifying_eigenvectors]: 量子ビットの一般の状態は実パラメータ2つで決まるので、$\sigma^{\theta, \phi}$などと書いたほうがより明示的ですが、ここでの議論では結局1パラメータしか使わないので、「何か一般の（次元を指定しない）パラメータ」として$\theta$と置いています。

## 問題: ベル状態と可分状態の違い

### 一般の$\sigma$演算子の期待値

上のように$R_y(\theta)\ket{0}$が固有値$+1$の固有ベクトルとなるような演算子を$\sigma^{\theta}$として、$\bra{\Psi} \sigma^{\phi}_B \sigma^{\theta}_A \ket{\Psi}$を計算してみましょう。まず基底変換を具体的に書き下します。

$$
\begin{align}
\ket{\Psi'} & = R_{y,B}(-\phi) R_{y,A}(-\theta) \frac{1}{\sqrt{2}} \left( \ket{00} + \ket{11} \right) \\
& = \frac{1}{\sqrt{2}} \left\{ \left[ \cos\left(\frac{\phi}{2}\right)\ket{0} - \sin\left(\frac{\phi}{2}\right)\ket{1} \right] \left[ \cos\left(\frac{\theta}{2}\right)\ket{0} - \sin\left(\frac{\theta}{2}\right)\ket{1} \right] + \left[ \sin\left(\frac{\phi}{2}\right)\ket{0} + \cos\left(\frac{\phi}{2}\right)\ket{1} \right] \left[ \sin\left(\frac{\theta}{2}\right)\ket{0} + \cos\left(\frac{\theta}{2}\right)\ket{1} \right] \right\} \\
& = \frac{1}{\sqrt{2}} \left[ \cos\left(\frac{\theta - \phi}{2}\right)\ket{00} - \sin\left(\frac{\theta - \phi}{2}\right)\ket{01} + \sin\left(\frac{\theta - \phi}{2}\right)\ket{10} + \cos\left(\frac{\theta - \phi}{2}\right)\ket{11} \right].
\end{align}
$$

したがって、

```{math}
:label: quantum_correlation
\begin{align}
\bra{\Psi} \sigma^{\phi}_B \sigma^{\theta}_A \ket{\Psi} & = \bra{\Psi'} \sigma^{0}_B \sigma^{0}_A \ket{\Psi'} \\
& = \cos^2\left(\frac{\theta - \phi}{2}\right) - \sin^2\left(\frac{\theta - \phi}{2}\right) \\
& = \cos(\theta - \phi)
\end{align}
```

となります。

### 問題

上の計算結果を量子回路でも確認してみましょう。実習のように2ビット量子レジスタをベル状態にし、2つの量子ビットに適当な$R_y$ゲートをかけ、期待値$C$を$R_y$ゲートのパラメータの値の差の関数としてプロットします。

```python
# Define theta and phi values
thetas = [0., np.pi / 4., np.pi / 2., 3. * np.pi / 4., np.pi]
theta_labels = ['0', r'$\pi/4$', r'$\pi/2$', r'$3\pi/4$', r'$\pi$']
phis = np.linspace(0., np.pi, 16, endpoint=True)

# Construct a circuit for each (theta, phi) pair
circuits = []
for itheta, theta in enumerate(thetas):
    for iphi, phi in enumerate(phis):
        circuit = QuantumCircuit(2, name='circuit_{}_{}'.format(itheta, iphi))

        ##################
        ### EDIT BELOW ###
        ##################

        #circuit.?

        ##################
        ### EDIT ABOVE ###
        ##################

        circuit.measure_all()

        circuits.append(circuit)

# Execute the circuit in qasm_simulator and retrieve the results
simulator = Aer.get_backend('qasm_simulator')
shots = 10000
sim_job = execute(circuits, backend=simulator, shots=shots)
result = sim_job.result()

# Plot C versus (theta - phi) for each theta
icirc = 0
for itheta, theta in enumerate(thetas):
    x = theta - phis
    y = np.zeros_like(x)

    for iphi, phi in enumerate(phis):
        c = result.get_counts(circuits[icirc])

        ##################
        ### EDIT BELOW ###
        ##################

        #y[iphi] = ?

        ##################
        ### EDIT ABOVE ###
        ##################

        icirc += 1

    plt.plot(x, y, 'o', label=theta_labels[itheta])

plt.legend()
plt.xlabel(r'$\theta - \phi$')
plt.ylabel(r'$\langle \Psi | \sigma^{\phi}_B \sigma^{\theta}_A | \Psi \rangle$')
```

式{eq}`quantum_correlation`では、2体系の期待値がパラメータ$\theta$と$\phi$だけを含む関数の積として記述できないことがはっきり示されています。これはベル状態の持つエンタングルメントの現れで、2体系をバラバラに考えることができないということを言っています。

それでは、$C$や$S$をエンタングルメントのない状態（可分状態）に対して計算してみたら、何が得られるでしょうか。例えば、ベル状態$1/\sqrt{2}(\ket{00} + \ket{11})$の代わりに、「確率1/2で$\ket{00}$、確率1/2で$\ket{11}$」という状態を考えます。

まずは$|S(\kappa, \lambda, \mu, \nu)|$の最大値を求めます。

```python
def classical_S(params):
    """
    Four-parameter function to be minimized. Returns -|S| = -|<sigma^k sigma^l> - <sigma^k sigma^n> + <sigma^m sigma^l> + <sigma^m sigma^n>|.

    Args:
        params (np.ndarray): Values of kappa, lambda, mu, nu

    Returns:
        float: -|S|
    """

    k, l, m, n = params

    S = 0.

    # S from |00>
    ##################
    ### EDIT BELOW ###
    ##################
    #sksl_00 =
    #sksn_00 =
    #smsl_00 =
    #smsn_00 =
    #S += (sksl_00 - sksn_00 + smsl_00 + smsn_00) * 0.5
    ##################
    ### EDIT ABOVE ###
    ##################

    # S from |11>
    ##################
    ### EDIT BELOW ###
    ##################
    #sksl_11 =
    #sksn_11 =
    #smsl_11 =
    #smsn_11 =
    #S += (sksl_11 - sksn_11 + smsl_11 + smsn_11) * 0.5
    ##################
    ### EDIT ABOVE ###
    ##################

    return -np.abs(S)

# Initial values
x0 = np.array([0., np.pi / 2., 0., np.pi / 2.])
# Bounds on all parameters ([0, pi])
bounds = Bounds([0.] * 4, [np.pi] * 4)
# Minimize using scipy.optimize.minimize
res = minimize(classical_S, x0, method='trust-constr', bounds=bounds)
print('argmax |S|: kappa={:.3f}pi, lambda={:.3f}pi, mu={:.3f}pi, nu={:.3f}pi'.format(*(res.x / np.pi)))
print('max |S|:', -res.fun)
```

この状態を量子回路で実装して、上の実験を繰り返してみましょう。ただし、実際に確率1/2で異なる状態が現れるような「混合状態」は量子回路では表現しにくいので、$\ket{00}$と$\ket{11}$を初期状態とする回路を一つずつ用意して、それらから得る$C$の平均値を求めることにします。

```python
# Construct a circuit for each (theta, phi) pair
circuits = []
for itheta, theta in enumerate(thetas):
    for iphi, phi in enumerate(phis):
        circuit_00 = QuantumCircuit(2, name='circuit_{}_{}_00'.format(itheta, iphi))
        circuit_11 = QuantumCircuit(2, name='circuit_{}_{}_11'.format(itheta, iphi))

        ##################
        ### EDIT BELOW ###
        ##################

        #circuit_00.?
        #circuit_11.?

        ##################
        ### EDIT ABOVE ###
        ##################

        circuit_00.measure_all()
        circuit_11.measure_all()

        circuits.append(circuit_00)
        circuits.append(circuit_11)

# Execute the circuit in qasm_simulator and retrieve the results
shots = 10000
sim_job = execute(circuits, backend=simulator, shots=shots)
result = sim_job.result()

# Plot C versus (theta - phi) for each theta
icirc = 0
for itheta, theta in enumerate(thetas):
    x = theta - phis
    y = np.zeros_like(x)

    for iphi, phi in enumerate(phis):
        c_00 = result.get_counts(circuits[icirc])
        c_11 = result.get_counts(circuits[icirc + 1])

        ##################
        ### EDIT BELOW ###
        ##################

        #y[iphi] = ?

        ##################
        ### EDIT ABOVE ###
        ##################

        icirc += 2

    plt.plot(x, y, 'o', label=theta_labels[itheta])

plt.legend()
plt.xlabel(r'$\theta - \phi$')
plt.ylabel(r'$\langle \Psi | \sigma^{\theta}_A \sigma^{\phi}_B | \Psi \rangle$')
```

ベル状態からの結果と比べてみて、何が言えるでしょうか。

**提出するもの**

- 完成した回路のコード（EDIT BELOW / EDIT ABOVEの間を埋める）とシミュレーション結果によるプロット
- ベル状態と可分状態の混合状態とでの2体相関の違いに関する考察


# Modeling spiking neural networks with Brian

```python
# If Brian2 is not yet installed, the following will install it
# (otherwise it will print a number of "Requirement already satisfied" lines)
%pip install brian2
```

    Requirement already satisfied: brian2 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (2.4.1)
    Requirement already satisfied: sympy>=1.2 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from brian2) (1.6.2)
    Requirement already satisfied: setuptools>=24.2 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from brian2) (49.6.0.post20201009)
    Requirement already satisfied: pyparsing in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from brian2) (2.4.7)
    Requirement already satisfied: jinja2>=2.7 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from brian2) (2.11.2)
    Requirement already satisfied: cython>=0.29 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from brian2) (0.29.21)
    Requirement already satisfied: numpy>=1.15 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from brian2) (1.19.2)
    Requirement already satisfied: mpmath>=0.19 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from sympy>=1.2->brian2) (1.1.0)
    Requirement already satisfied: MarkupSafe>=0.23 in /mnt/data/anaconda2/envs/datasci_course/lib/python3.7/site-packages (from jinja2>=2.7->brian2) (1.1.1)
    Note: you may need to restart the kernel to use updated packages.

Let's first import "everything" from the Brian 2 package.

This also provides access to the scientific computing package numpy (imported as np), and to the package pyplot from the plotting library matplotlib (imported as plt).

We also ask the notebook to include plots directly in the notebook (instead of showing them in a separate window). Note that lines starting with % are specific commands for the jupyter notebook, they won't work in a Python script, for example.

We also switch off Brian's "code generation" mechanism, which improves the performance for complex models by generating/compiling/executing C++ code "behind the scenes". For the simple models that we are covering in this tutorial, this is not necessary – it even slows down things due to the need for compilation.

```python
from brian2 import *
prefs.codegen.target = 'numpy'
```

```python
%matplotlib inline
%xmode minimal
```

    Exception reporting mode: Minimal

## Neurons

Let's create a group of integrate-and-fire neurons. We use the same equation as in last week's tutorial
$$
\tau\frac{\mathrm{d}V}{\mathrm{d}t} = E_L - V + I_\mathrm{stim}
$$
If $V > V_\mathrm{threshold}$: emit a spike and set $V \leftarrow V_\mathrm{reset}$.

```python
start_scope()
N = 3
E_L = -50*mV
V_threshold = -30*mV
V_reset = -55*mV
C = 70*pF
g_L = 10*nS
tau = C/g_L
duration = 100*ms
neurons = NeuronGroup(N, '''dV/dt = (E_L - V + I_stim)/tau : volt
                            I_stim : volt (constant)  # note, this is rescaled with g_L already''',
                      threshold='V>V_threshold', reset='V=V_reset', method='euler')
# Initialize values
neurons.V = V_reset
# neurons.I_stim = ...

# record membrane potential and spikes
v_mon = StateMonitor(neurons, 'V', record=True)
spike_mon = SpikeMonitor(neurons)

# run simulation
run(duration)
```

We could plot things directly by calling `plt.plot`, but by calling `plt.subplots` first we can easily arrange things in subplots.

```python
fig, ax = plt.subplots(2, 1, sharex=True)
ax[0].plot(spike_mon.t/ms, spike_mon.i, 'o')
ax[1].plot(v_mon.t/ms, v_mon.V.T/mV);
```

### f/I curve

```python
# Same code as before. What do we have to change to plot an f/I curve?

start_scope()
N = 3
E_L = -50*mV
V_threshold = -30*mV
V_reset = -55*mV
C = 70*pF
g_L = 10*nS
tau = C/g_L
duration = 100*ms
neurons = NeuronGroup(N, '''dV/dt = (E_L - V + I_stim)/tau : volt
                            I_stim : volt (constant)  # note, this is rescaled with g_L already''',
                      threshold='V>V_threshold', reset='V=V_reset', method='euler')
# Initialize values
neurons.V = V_reset
# neurons.I_stim = ...

# record membrane potential and spikes
v_mon = StateMonitor(neurons, 'V', record=True)
spike_mon = SpikeMonitor(neurons)

# run simulation
run(duration)
```

## Synapses

As mentioned in the lecture, synapses can be modeled on various models of detail. Here, we will use an "exponential current-based" synapse. Let us first try this out with some artficial stimulation:

```python
start_scope()
E_L = -50*mV
V_threshold = -30*mV
V_reset = -55*mV
C = 70*pF
g_L = 10*nS
tau = C/g_L
tau_syn = 2*ms
duration = 100*ms

# Artificial stimulation
exc_stim = SpikeGeneratorGroup(1, [0, 0], [10, 85]*ms)
inh_stim = SpikeGeneratorGroup(1, [0, 0], [30, 80]*ms)

# 3 Neurons all connected to the same artificial input spike trains
# The first neuron only gets excitatory input, the second only inhibitory, the third gets both
neurons = NeuronGroup(3, '''dV/dt = (E_L - V + I_syn)/tau : volt
                            dI_syn/dt = -I_syn/tau_syn : volt # decay between spikes''',
                      threshold='V>V_threshold', reset='V=V_reset', method='euler')
# Initialize values
neurons.V = E_L

# Introduce synaptic connections
exc_syn = Synapses(exc_stim, neurons, on_pre='I_syn_post += 1*mV')  # "on_pre" = "for each pre-synaptic spike"
exc_syn.connect(i=[0, 0], j=[0, 2])  # i = "source index", j = "target index"
inh_syn = Synapses(inh_stim, neurons, on_pre='I_syn_post -= 1*mV')
inh_syn.connect(i=[0, 0], j=[1, 2])

# record membrane potential and spikes
v_mon = StateMonitor(neurons, 'V', record=True)
spike_mon = SpikeMonitor(neurons)

# run simulation
run(duration)
```

```python
fig, axs = plt.subplots(3, 1, sharex=True)
axs[0].plot(v_mon.t/ms, v_mon.V[0]/mV)
axs[1].plot(v_mon.t/ms, v_mon.V[1]/mV)
axs[2].plot(v_mon.t/ms, v_mon.V[2]/mV);
```

### Network

```python
# This does not do anything interesting yet

start_scope()
N_E = 4000  # excitatory neurons
N_I = 1000  # inhibitory neurons
E_L = -50*mV
V_threshold = -30*mV
V_reset = -55*mV
C = 70*pF
g_L = 10*nS
tau = C/g_L
tau_syn = 2*ms
w_e = 1*mV
w_i = 1*mV
duration = 1000*ms

# Artificial stimulation
exc_stim = SpikeGeneratorGroup(1, [0, 0], [10, 85]*ms)
inh_stim = SpikeGeneratorGroup(1, [0, 0], [30, 80]*ms)

# Now a single "receiver neuron"
neurons = NeuronGroup(N_E + N_I,
                      '''dV/dt = (E_L - V + I_syn)/tau : volt
                         dI_syn/dt = -I_syn/tau_syn : volt # decay between spikes''',
                      threshold='V>V_threshold', reset='V=V_reset', method='euler')
# Initialize values
neurons.V = E_L

exc_neurons = neurons[:N_E]  # Uses "slicing" to get subpopulations of neurons
inh_neurons = neurons[N_E:]

# Connect neurons to each other
exc_syn = Synapses(exc_neurons, neurons, on_pre='I_syn_post += w_e')  # "on_pre" = "for each pre-synaptic spike"
exc_syn.connect(p=0.02)  # 2% connection probability between each pair of neurons
inh_syn = Synapses(inh_neurons, neurons, on_pre='I_syn_post -= w_i')
inh_syn.connect(p=0.02)

# record spikes of all neurons, but the membrane potential only of 1 neuron
v_mon = StateMonitor(neurons, 'V', record=0)
spike_mon = SpikeMonitor(neurons)

# run simulation
run(duration, report='text')  # for simulations that take longer to run
```

    Starting simulation at t=0. s for a duration of 1. s
    1. s (100%) simulated in 1s

```python
fig, ax = plt.subplots(2, 1, sharex=True, figsize=(12, 6))
ax[0].plot(spike_mon.t/ms, spike_mon.i, '.')
ax[1].plot(v_mon.t/ms, v_mon.V.T/mV);
```


This is an IPyton notebook and it is coded in the Python
programming language.

Python has the idea of `modules`, separate units of code that
provide the programmer with _ready to use_, tested code.
You can write yourself your modules,  or rather use library
modules, in any case you access _functions_ or _data_ defined in
modules `import`-ing them.

For example, in the next _code cell_ we import three functions
from the `math` module (and a constant too) and then we do some
other stuff...

 1. `import` functions and data from `math`,
 2. `import` all the stuff from `sympy` (symbolic algebra) under
 the prefix `sy`, and use this convention to initialize the `sympy`
 printing system,
 3. `import` from the submodule `IPython.display` two utility
 functions,
 4. eventually `import` the module `style` and execute its
 function `clean` that changes the default appearance of a notebook
 using some `css` styling and a small sprinkle of `javascript`.

```
%pylab --no-import-all inline

from math import exp, log, pi, sqrt

import sympy as sy ; sy.init_printing(use_latex=True)
from IPython.display import display, Latex

import style ; style.clean()
```

    Populating the interactive namespace from numpy and matplotlib

<style type="text/css">
.container { width:100% !important; }
div#notebook { /* centers the page content */
padding-left:  2%;
padding-right:2%;
}
div.input {
width: 83ex; /* about 80 chars + buffer */
}

div.text_cell {
width: 83ex /* instead of 100%, */
}

div.text_cell_render {
font-family:  Candara,Cambria, serif;
font-size: 1.5em
line-height: 145%; /* added for some line spacing of text. */
width: 83ex; /* instead of 'inherit' for shorter lines */
}

/* Set the size of the headers */
div.text_cell_render h1 {
font-size: 24pt;
}

div.text_cell_render h2 {
font-size: 20pt;
}

.CodeMirror {
font-family: Consolas, monospace;
width: 83ex;
}

.rendered_html ol {list-style:decimal; margin: 1em 2em;}

</style>

# Damped response for a given $x_0$

What is the analytical expression of the response of a damped system
subject to the (quite common) initial conditions $x(0)=x_0$ and $\dot x(0)=0$?

We start by creating a set of `symbols` and giving them good names...

```
A, B, t, z, wn, wd, x0 = sy.symbols(
    'A, B, t, zeta, omega_n, omega_d, x_0')
display((A, B, t, z, wn, wd, x0))
```

Then we give the name `u` to the symbolic representation of the displacement,
and the name `v` to the first time derivative of `u`.

```
u = sy.exp(-z*wn*t)*(A*sy.cos(wd*t)+B*sy.sin(wd*t))
v = u.diff(t).expand().collect(sy.exp(-z*wn*t))
v = v.collect(sy.cos(wd*t)).collect(sy.sin(wd*t))
display(Latex(r'\begin{align*}x(t)&='+sy.latex(u)+r'''\\
\\dot{x}(t) &= '''+sy.latex(v)+r'\end{align*}'))
```

\begin{align*}x(t)&=\left(A \cos{\left (\omega_{d} t \right )} + B \sin{\left (\omega_{d} t \right )}\right) e^{- \omega_{n} t \zeta}\\\dot{x}(t) &= \left(\left(- A \omega_{d} - B \omega_{n} \zeta\right) \sin{\left (\omega_{d} t \right )} + \left(- A \omega_{n} \zeta + B \omega_{d}\right) \cos{\left (\omega_{d} t \right )}\right) e^{- \omega_{n} t \zeta}\end{align*}

Now,

 - we form an equation substituting $t=0$ in `u` and `v` and subtracting $x_0$
   from the first equation
 - we ask our `sympy` aka `sy` library to solve the system of equations

```
eq = (u.subs(t,0)-x0, v.subs(t,0))
coeff = sy.solve(eq, A,B)
display(coeff)
display(u.subs(coeff).collect(x0))
display(u.subs(coeff).collect(x0).subs(wn,wd/sy.sqrt(1-z*z)))
```

To plot the response we need to get rid of all the symbols in our expression,
except for $t$, so we choose an arbitrary set of values for the free variables...

```
values = {x0:1.0, z:0.05, wd:2*sy.pi, wn:2*sy.pi/sy.sqrt(1-z*z)}
values[sy.pi] = sy.pi.evalf()
display(values)
```

At the end, we display the analytical expression of $x(t)$ and plot

 1. $x(t)$,

 2. $\dot x(t)$.

```
display(u.subs(coeff).subs(values))
sy.plot(u.subs(coeff).subs(values), (t,0,5.99));
sy.plot(v.subs(coeff).subs(values), (t,0,5.99)  );
```

# Logarithmic decrement

We assume that our system has been deformed by the application of some static load,
so that at time $t=0$ the initial conditions are $x(0)=x_0$ and $\dot x(0)=0$.

At $t=0$ the static force is suddenly released, we observe an oscillatory behaviour
as we have detailed in the preceding section.

and we are able to measure the peak displacement after $n$ cycles, $x_n$.

## Specify data

The following code gives appropriate names to values measured or computed from
expressions, the value of the logarithmic decrement is eventually printed.

```
n = 3
x0 = 8.00
xn = 3.11
dn = log(x0/xn)
print "delta_n =",dn
```

    delta_n = 0.944818815489

The exact expression for $\zeta$ is

$$  \zeta = \sqrt{\frac{\delta_n^2}{(2 n \pi)^2 + \delta_n^2}}
  \text{ with }  \delta_n = \log\frac{x_0}{x_n}.$$

We've already computed $\delta_n$ in the previous code cell
so we can write the numerator and the denominator of the radicand
and eventually compute the _"exact"_ value of $\zeta$.

```
num = dn*dn
den = 4 * n*n * pi*pi + dn*dn
z = sqrt(num/den)
print "zeta = %f"%(z*100,)
```

    zeta = 5.006135

## Approximate expression

The linearized expression for $\zeta$, that we have found using the first
and second terms of the McLaurin series expressing $\exp(a\zeta)=1+a\zeta$, is

$$\zeta \approx \frac{1}{2 n \pi}\,\frac{x_0-x_n}{x_n}.$$

We know that this approximation is good for low damping and few cycles...

```
print "Estimate", 100 * (x0-xn) / xn / ( 2*n* pi )
print "error (z/est.)",z /((x0-xn) / xn / ( 2*n* pi ))
```

    Estimate 8.34156132604
    error (z/est.) 0.600143600147

## Iterative solution

We write, formally,

$$\zeta = \xi\sqrt{1-\zeta^2}, \qquad\text{with }\xi = \frac{1}{2 n \pi} \log\frac{x_0}{x_n}$$

and derive an iterative formulation

$$\zeta_{i+1} = \xi\sqrt{1-\zeta_i^2}, \qquad\text{with } i = 0, 1, 2, \ldots$$

Note that, for $\zeta_0 = 0$, it is $\zeta_1 = \xi$.

In theory, we have to check the convergence of the sequence and stop when the
convergence is reached, but in our cases the convergence is soo fast that we
are safe implementing the solution in a `for` loop

```
csi= dn/2/n/pi
z_i = csi

print "      zeta_0 = 0.00000000000000%"
print "csi = zeta_1 = %16.14f%%" % (csi*100,)
for i in range(2, 6):
    z_i = csi*sqrt(1-z_i*z_i)
    print '      zeta_%d = %16.14f%%'%(i,100*z_i)
```

          zeta_0 = 0.00000000000000%
    csi = zeta_1 = 5.01241949370850%
          zeta_2 = 5.00611884484703%
          zeta_3 = 5.00613468475389%
          zeta_4 = 5.00613464495722%
          zeta_5 = 5.00613464505721%


# 深度学习工具 PyTorch 简介

在此 notebook 中，你将了解 [PyTorch](http://pytorch.org/)，一款用于构建和训练神经网络的框架。PyTorch 在很多方面都和 Numpy 数组很像。毕竟，这些 Numpy 数组也是张量。PyTorch 会将这些张量当做输入并使我们能够轻松地将张量移到 GPU 中，以便在训练神经网络时加快处理速度。它还提供了一个自动计算梯度的模块（用于反向传播），以及另一个专门用于构建神经网络的模块。总之，与 TensorFlow 和其他框架相比，PyTorch 与 Python 和 Numpy/Scipy 堆栈更协调。

## 神经网络

深度学习以人工神经网络为基础。人工神经网络大致产生于上世纪 50 年代末。神经网络由多个像神经元一样的单个部分组成，这些部分通常称为单元或直接叫做“神经元”。每个单元都具有一定数量的加权输入。我们对这些加权输入求和，然后将结果传递给激活函数，以获得单元的输出。

数学公式如下所示：

$$
\begin{align}
y &= f(w_1 x_1 + w_2 x_2 + b) \\
y &= f\left(\sum_i w_i x_i +b \right)
\end{align}
$$

对于向量来说，为两个向量的点积/内积：

$$
h = \begin{bmatrix}
x_1 \, x_2 \cdots  x_n
\end{bmatrix}
\cdot
\begin{bmatrix}
           w_1 \\
           w_2 \\
           \vdots \\
           w_n
\end{bmatrix}
$$

## 张量

实际上神经网络计算只是对*张量*进行一系列线性代数运算，张量是矩阵的泛化形式。向量是一维张量，矩阵是二维张量，包含 3 个索引的数组是三维张量（例如 RGB 彩色图像）。神经网络的基本数据结构是张量，PyTorch（以及几乎所有其他深度学习框架）都是以张量为基础。

这些是基本知识，我们现在来看 PyTorch 如何构建简单的神经网络。

```python
# First, import PyTorch
import torch
```

```python
def activation(x):
    """ Sigmoid activation function

        Arguments
        ---------
        x: torch.Tensor
    """
    return 1/(1+torch.exp(-x))
```

```python
### Generate some data
torch.manual_seed(7) # Set the random seed so things are predictable

# Features are 3 random normal variables
features = torch.randn((1, 5))
# True weights for our data, random normal variables again
weights = torch.randn_like(features)
# and a true bias term
bias = torch.randn((1, 1))
```

我在上面生成了一些数据，我们可以使用该数据获取这个简单网络的输出。这些暂时只是随机数据，之后我们将使用正常数据。我们来看看：

`features = torch.randn((1, 5))` 创建一个形状为 `(1, 5)` 的张量，其中有 1 行和 5 列，包含根据正态分布（均值为 0，标准偏差为 1）随机分布的值。

`weights = torch.randn_like(features)` 创建另一个形状和 `features` 一样的张量，同样包含来自正态分布的值。

最后，`bias = torch.randn((1, 1))` 根据正态分布创建一个值。

和 Numpy 数组一样，PyTorch 张量可以相加、相乘、相减。行为都很类似。但是 PyTorch 张量具有一些优势，例如 GPU 加速，稍后我们会讲解。请计算这个简单单层网络的输出。
> **练习**：计算网络的输出：输入特征为 `features`，权重为 `weights`，偏差为 `bias`。和 Numpy 类似，PyTorch 也有一个对张量求和的 [`torch.sum()`](https://pytorch.org/docs/stable/torch.html#torch.sum) 函数和 `.sum()` 方法。请使用上面定义的函数 `activation` 作为激活函数。

```python
## Calculate the output of this network using the weights and bias tensors
```

你可以在同一运算里使用矩阵乘法进行乘法和加法运算。推荐使用矩阵乘法，因为在 GPU 上使用现代库和高效计算资源使矩阵乘法更高效。

如何对特征和权重进行矩阵乘法运算？我们可以使用 [`torch.mm()`](https://pytorch.org/docs/stable/torch.html#torch.mm) 或 [`torch.matmul()`](https://pytorch.org/docs/stable/torch.html#torch.matmul)，后者更复杂，并支持广播。如果不对`features` 和 `weights` 进行处理，就会报错：

```python
>> torch.mm(features, weights)

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-13-15d592eb5279> in <module>()
----> 1 torch.mm(features, weights)

RuntimeError: size mismatch, m1: [1 x 5], m2: [1 x 5] at /Users/soumith/minicondabuild3/conda-bld/pytorch_1524590658547/work/aten/src/TH/generic/THTensorMath.c:2033
```

在任何框架中构建神经网络时，我们都会频繁遇到这种情况。原因是我们的张量不是进行矩阵乘法的正确形状。注意，对于矩阵乘法，第一个张量里的列数必须等于第二个张量里的行数。`features` 和 `weights` 具有相同的形状，即 `(1, 5)`。意味着我们需要更改 `weights` 的形状，以便进行矩阵乘法运算。

**注意：**要查看张量 `tensor` 的形状，请使用 `tensor.shape`。以后也会经常用到。

现在我们有以下几个选择：[`weights.reshape()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.reshape)、[`weights.resize_()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.resize_) 和 [`weights.view()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.view)。

* `weights.reshape(a, b)` 有时候将返回一个新的张量，数据和 `weights` 的一样，大小为 `(a, b)`；有时候返回克隆版，将数据复制到内存的另一个部分。
* `weights.resize_(a, b)` 返回形状不同的相同张量。但是，如果新形状的元素数量比原始张量的少，则会从张量里删除某些元素（但是不会从内存中删除）。如果新形状的元素比原始张量的多，则新元素在内存里未初始化。注意，方法末尾的下划线表示这个方法是**原地**运算。要详细了解如何在 PyTorch 中进行原地运算，请参阅[此论坛话题](https://discuss.pytorch.org/t/what-is-in-place-operation/16244)。
* `weights.view(a, b)` 将返回一个张量，数据和 `weights` 的一样，大小为 `(a, b)`。

我通常使用 `.view()`，但这三个方法对此示例来说都可行。现在，我们可以通过 `weights.view(5, 1)` 变形 `weights`，使其具有 5 行和 1 列。

> **练习**：请使用矩阵乘法计算网络的输出

```python
## Calculate the output of this network using matrix multiplication
```

### 堆叠

这就是计算单个神经元的输出的方式。当你将单个单元堆叠为层，并将层堆叠为神经元网络后，你就会发现这个算法的强大之处。一个神经元层的输出变成下一层的输入。对于多个输入单元和输出单元，我们现在需要将权重表示为矩阵。

底部显示的第一个层级是输入，称为**输入层**。中间层称为**隐藏层**，最后一层（右侧）是**输出层**。我们可以再次使用矩阵从数学角度来描述这个网络，然后使用矩阵乘法将每个单元线性组合到一起。例如，可以这样计算隐藏层（$h_1$ 和 $h_2$）：

$$
\vec{h} = [h_1 \, h_2] =
\begin{bmatrix}
x_1 \, x_2 \cdots \, x_n
\end{bmatrix}
\cdot
\begin{bmatrix}
           w_{11} & w_{12} \\
           w_{21} &w_{22} \\
           \vdots &\vdots \\
           w_{n1} &w_{n2}
\end{bmatrix}
$$

我们可以将隐藏层当做输出单元的输入，从而得出这个小网络的输出，简单表示为：

$$
y =  f_2 \! \left(\, f_1 \! \left(\vec{x} \, \mathbf{W_1}\right) \mathbf{W_2} \right)
$$

```python
### Generate some data
torch.manual_seed(7) # Set the random seed so things are predictable

# Features are 3 random normal variables
features = torch.randn((1, 3))

# Define the size of each layer in our network
n_input = features.shape[1]     # Number of input units, must match number of input features
n_hidden = 2                    # Number of hidden units
n_output = 1                    # Number of output units

# Weights for inputs to hidden layer
W1 = torch.randn(n_input, n_hidden)
# Weights for hidden layer to output layer
W2 = torch.randn(n_hidden, n_output)

# and bias terms for hidden and output layers
B1 = torch.randn((1, n_hidden))
B2 = torch.randn((1, n_output))
```

> **练习：**使用权重 `W1` 和 `W2` 以及偏差 `B1` 和 `B2` 计算此多层网络的输出。

```python
## Your solution here
```

如果计算正确，输出应该为 `tensor([[ 0.3171]])`。

隐藏层数量是网络的参数，通常称为**超参数**，以便与权重和偏差参数区分开。稍后当我们讨论如何训练网络时会提到，层级越多，网络越能够从数据中学习规律并作出准确的预测。

## Numpy 和 Torch 相互转换

加分题！PyTorch 可以实现 Numpy 数组和 Torch 张量之间的转换。Numpy 数组转换为张量数据，可以用 `torch.from_numpy()`。张量数据转换为 Numpy 数组，可以用 `.numpy()` 。

```python
import numpy as np
a = np.random.rand(4,3)
a
```

```python
b = torch.from_numpy(a)
b
```

```python
b.numpy()
```

Numpy 数组与 Torch 张量之间共享内存，因此如果你原地更改一个对象的值，另一个对象的值也会更改。

```python
# Multiply PyTorch Tensor by 2, in place
b.mul_(2)
```

```python
# Numpy array matches new values from Tensor
a
```

deep-learning-v2-pytorch/intro-to-pytorch/Part 1 - Tensors in PyTorch (Exercises)-cn.ipynb


# Model Checking

After running an MCMC simulation, `sample` returns an object (as of PyMC 3.9, an `InferenceData` object) containing the samples for all the stochastic and deterministic random variables. The final step in Bayesian computation is model checking, in order to ensure that inferences derived from your sample are valid.

There are **two components** to model checking:

1. Convergence diagnostics
2. Goodness of fit

Convergence diagnostics are intended to detect **lack of convergence** in the Markov chain Monte Carlo sample; it is used to ensure that you have not halted your sampling too early. However, a converged model is not guaranteed to be a good model.

The second component of model checking, goodness of fit, is used to check the **internal validity** of the model, by comparing predictions from the model to the data used to fit the model.

```python
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
import scipy.stats as st
import pymc3 as pm
import theano.tensor as tt
import arviz as az
import warnings
warnings.simplefilter("ignore")

RANDOM_SEED = 20090425
```

## Convergence Diagnostics

There are a handful of easy-to-use methods for checking convergence. Since you cannot prove convergence, but only show lack of convergence, there is no single method that is foolproof. So, its best to look at a suite of diagnostics together.

We will cover the canonical set of checks:

- Traceplot
- Divergences
- R-hat
- Effective Sample Size

## Traceplot

This is a simple plot that is a good quick check to make sure nothing is obviously wrong, and is usually the first diagnostic step you will take. You've seen these already: just the time series of samples for an individual variable.

Let's run the PKU model again as an example:

```python
pku_data = pd.read_csv('../data/pku_data.csv')

unique_papers = set(pku_data['Paper ID'])
paper_map = {p:i for i,p in enumerate(unique_papers)}
paper_id = pku_data['Paper ID'].replace(paper_map)
phe_std = ((pku_data.Phe - pku_data.Phe.mean()) / pku_data.Phe.std()).values
iq = pku_data['IQ'].values
concurrent_measurement = pku_data['Concurrent'].values

with pm.Model() as pku_model:

    μ_int = pm.Normal('μ_int', mu=100, sigma=1e3)
    σ_int = pm.HalfCauchy('σ_int', beta=5)
    β_0 = pm.Normal('β_0', μ_int, sigma=σ_int, shape=len(unique_papers))
    β_1 = pm.Normal('β_1', mu=0, sigma=1e3)
    β_2 = pm.Normal('β_2', mu=0, sigma=1e3)

    μ_iq = β_0[paper_id] + β_1*phe_std + β_2*concurrent_measurement

    σ_iq = pm.HalfCauchy('σ_iq', beta=1)
    iq_like = pm.Normal('iq_like', mu=μ_iq, sigma=σ_iq, observed=iq)
```

```python
with pku_model:

    pku_trace = pm.sample(500, tune=0, step=pm.Metropolis(), return_inferencedata=True, random_seed=RANDOM_SEED)
```

The `plot_trace` function from ArViZ by default generates a kernel density plot and a trace plot, with a different color for each chain of the simulation.

```python
az.plot_trace(pku_trace, var_names=['μ_int', 'σ_iq']);
```

This sample is deliberately inadequate. Looking at the trace plot, the problems should be apparent.

Can you identify the issues, based on what you learned in the previous section?

### Exercise: Take a quiz!

[See how well you can identify sampling problems by looking at their traceplots](https://canyon289.github.io/bayesian-model-evaluation/lessonplans/mcmc_basics/#/14)

The slides will show you a trace, and you have to guess whether the sampling is from one of:

- MCMC with step size too small
- MCMC with step size too large
- MCMC with adequate step size
- Independent samples from distribution

## Divergences

As we have seen, Hamiltonian Monte Carlo (and NUTS) performs numerical integration in order to explore the posterior distribution of a model. When the integration goes wrong, it can go dramatically wrong.

For example, here are some Hamiltonian trajectories on the distribution of two correlated variables. Can you spot the divergent path?

The reason that this happens is that there may be parts of the posterior which are **hard to explore** for geometric reasons. Two ways of solving divergences are

1. **Set a higher "target accept" rate**: Similarly (but not the same) as for Metropolis-Hastings, larger integrator steps lead to lower acceptance rates. A higher `target_accept` will generally cause a smaller step size, and more accurate integration.
2. **Reparametrize**: If you can write your model in a different way that has the same joint probability density, you might do that. A lot of work is being done to automate this, since it requires careful work, and one goal of a probabilistic programming language is to iterate quickly. See [Hoffmann, Johnson, Tran (2018)](https://arxiv.org/abs/1811.11926), [Gorinova, Moore, Hoffmann (2019)](https://arxiv.org/abs/1906.03028), and there is work on this also in [symbolic pymc](https://github.com/pymc-devs/symbolic-pymc).

You should be wary of a trace that contains many divergences (particularly those clustered in particular regions of the parameter space), and give thought to how to fix them.

### Divergence example

The trajectories above are from a famous example of a difficult geometry: Neal's funnel. It is problematic because the geometry is very different in some regions of the state space relative to others. Specifically, for hierarchical models, as the scale parameter changes in size so do the values of the parameters it is constraining. When the variance is close to zero, the parameter space is very constrained relative to the majority of the support.

```python
def neals_funnel(dims=1):
    with pm.Model() as funnel:
        v = pm.Normal('v', 0, 3)
        x_vec = pm.MvNormal('x_vec', mu=tt.zeros(dims), cov=2 * tt.exp(v) * tt.eye(dims), shape=dims)
    return funnel

with neals_funnel():
    funnel_trace = pm.sample(random_seed=RANDOM_SEED, return_inferencedata=False)
```

PyMC3 provides us feedback on divergences, including a count and a recommendation on how to address them.

```python
diverging_ind = funnel_trace['diverging'].nonzero()[0]
diverging_ind
```

```python
ax, *_ = pm.plot_joint(trace)
ax.plot(trace['v'][diverging_ind], trace['x_vec'][diverging_ind], 'y.');
```

Let's look at an example of this using the radon example from the first section. Specifically, we will run the random-slopes model, which has a hierarchical model for the basement effect or radon measurements.

```python
# Import radon data
radon_data = pd.read_csv('../data/radon.csv', index_col=0)

counties = radon_data.county.unique()
n_counties = counties.shape[0]
county = radon_data.county_code.values
log_radon = radon_data.log_radon.values
floor_measure = radon_data.floor.values
log_uranium = np.log(radon_data.Uppm.values)
county_lookup = dict(zip(counties, np.arange(n_counties)))
```

```python
with pm.Model() as varying_slope:

    # Priors
    μ_b = pm.Normal('μ_b', mu=0., sigma=10)
    σ_b = pm.HalfCauchy('σ_b', 5)

    # Common intercepts
    a = pm.Normal('a', mu=0., sigma=10)
    # Random slopes
    b = pm.Normal('b', mu=μ_b, sigma=σ_b, shape=n_counties)

    # Model error
    σ_y = pm.HalfCauchy('σ_y',5)

    # Expected value
    y_hat = a + b[county] * floor_measure

    # Data likelihood
    y_like = pm.Normal('y_like', mu=y_hat, sigma=σ_y, observed=log_radon)

```

```python
with varying_slope:
    varying_slope_trace = pm.sample(2000, tune=1000, cores=2, random_seed=RANDOM_SEED)
```

If we examine the traces of the slope variance and any one of the county slopes, we can see a pathology when the group variance gets very small.

```python
fig, axs = plt.subplots(nrows=2)
axs[0].plot(varying_slope_trace.get_values('σ_b', chains=0), alpha=.5);
axs[0].set(ylabel='σ_b');
axs[1].plot(varying_slope_trace.get_values('b', chains=0), alpha=.05);
axs[1].set(ylabel='b');
```

Notice that when the chain reaches the lower end of the parameter space for $\sigma_b$, it appears to get "stuck" and the entire sampler, including the random slopes `b`, mixes poorly.

Jointly plotting the random effect variance and one of the individual random slopes demonstrates what is going on.

```python
x = pd.Series(varying_slope_trace['b'][:, 10], name='slope')
y = pd.Series(varying_slope_trace['σ_b'], name='slope group variance')
diverging = varying_slope_trace['diverging']

jp = sns.jointplot(x, y, ylim=(0, .7), stat_func=None, alpha=0.3)
jp.ax_joint.plot(x[diverging], y[diverging], 'yo');
```

When the group variance is small, this implies that the individual random slopes are themselves close to the group mean. In itself, this is not a problem, since this is the behavior we expect. However, if the sampler is tuned for the wider (unconstrained) part of the parameter space, it has trouble in the areas of higher curvature. The consequence of this is that the neighborhood close to the lower bound of $\sigma_b$ is sampled poorly; indeed, in our chain it is not sampled at all below 0.1. The result of this will be biased inference.

The `plot_parallel` function in the ArViZ library is a convenient way to identify patterns in divergent traces:

```python
az.plot_parallel(varying_slope_trace, var_names=['b'], figsize=(12,5));
```

Now that we've spotted the problem, what can we do about it? The best way to deal with this issue is to reparameterize our model.

### Solution: Non-centered Parameterization

The partial pooling model specified above uses a **centered** parameterization of the slope random effect. That is, the individual county effects are distributed around a county mean, with a spread controlled by the hierarchical standard deviation parameter.

Here is the DAG of this centered model:

```python
pm.model_to_graphviz(varying_slope)
```

We can remove the issue with sampling geometry by **reparameterizing** our model:

```python
with pm.Model() as varying_slope_noncentered:

    # Priors
    μ_b = pm.Normal('μ_b', mu=0., sigma=10)
    σ_b = pm.HalfCauchy('σ_b', 5)

    # Common intercepts
    a = pm.Normal('a', mu=0., sigma=10)

    # Non-centered random slopes
    # Centered: b = Normal('b', μ_b, sigma=σ_b, shape=counties)
    υ = pm.Normal('υ', mu=0, sigma=1, shape=n_counties)
    b = pm.Deterministic("b", μ_b + υ * σ_b)

    # Model error
    σ_y =pm.HalfCauchy('σ_y',5)

    # Expected value
    y_hat = a + b[county] * floor_measure

    # Data likelihood
    y_like = pm.Normal('y_like', mu=y_hat, sigma=σ_y, observed=log_radon)

```

This is a **non-centered** parameterization. By this, we mean that the random deviates are no longer explicitly modeled as being centered on $\mu_b$. Instead, they are independent standard normals $\upsilon$, which are then scaled by the appropriate value of $\sigma_b$, before being location-transformed by the mean.

```python
pm.model_to_graphviz(varying_slope_noncentered)
```

This model samples much better.

```python
with varying_slope_noncentered:
    noncentered_trace = pm.sample(2000, tune=1000, cores=2, random_seed=RANDOM_SEED)
```

Notice that the bottlenecks in the traces are gone.

```python
fig, axs = plt.subplots(nrows=2)
axs[0].plot(noncentered_trace.get_values('σ_b', chains=0), alpha=.5);
axs[0].set(ylabel='σ_b');
axs[1].plot(noncentered_trace.get_values('b', chains=0), alpha=.5);
axs[1].set(ylabel='b');
```

And, we are now fully exploring the support of the posterior.

```python
x = pd.Series(noncentered_trace['b'][:, 75], name='slope')
y = pd.Series(noncentered_trace['σ_b'], name='slope group variance')

sns.jointplot(x, y, ylim=(0, .7), stat_func=None);
```

```python
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
pm.plot_posterior(varying_slope_trace, var_names=['σ_b'], ax=ax1, color='LightSeaGreen')
pm.plot_posterior(noncentered_trace, var_names=['σ_b'], ax=ax2, color='LightSeaGreen')
ax1.set_title('Centered (top) and non-centered (bottom)')
plt.tight_layout()
```

## Potential Scale Reduction: $\hat{R}$

Roughly, $\hat{R}$ (*R-Hat*, or the *Gelman-Rubin statistic*) is the ratio of between-chain variance to within-chain variance. This diagnostic uses multiple chains to
check for lack of convergence, and is based on the notion that if
multiple chains have converged, by definition they should appear very
similar to one another; if not, one or more of the chains has failed to
converge.

$\hat{R}$ uses an analysis of variance approach to
assessing convergence. That is, it calculates both the between-chain
varaince (B) and within-chain varaince (W), and assesses whether they
are different enough to worry about convergence. Assuming $m$ chains,
each of length $n$, quantities are calculated by:

$$\begin{align}B &= \frac{n}{m-1} \sum_{j=1}^m (\bar{\theta}_{.j} - \bar{\theta}_{..})^2 \\
W &= \frac{1}{m} \sum_{j=1}^m \left[ \frac{1}{n-1} \sum_{i=1}^n (\theta_{ij} - \bar{\theta}_{.j})^2 \right]
\end{align}$$

for each scalar estimand $\theta$. Using these values, an estimate of
the marginal posterior variance of $\theta$ can be calculated:

$$\hat{\text{Var}}(\theta | y) = \frac{n-1}{n} W + \frac{1}{n} B$$

Assuming $\theta$ was initialized to arbitrary starting points in each
chain, this quantity will overestimate the true marginal posterior
variance. At the same time, $W$ will tend to underestimate the
within-chain variance early in the sampling run. However, in the limit
as $n \rightarrow
\infty$, both quantities will converge to the true variance of $\theta$.
In light of this, $\hat{R}$ monitors convergence using
the ratio:

$$\hat{R} = \sqrt{\frac{\hat{\text{Var}}(\theta | y)}{W}}$$

This is called the **potential scale reduction**, since it is an estimate of
the potential reduction in the scale of $\theta$ as the number of
simulations tends to infinity. In practice, we look for values of
$\hat{R}$ close to one (say, less than 1.1) to be confident that a
particular estimand has converged.

In ArViZ, the `summary` table, or a `plot_forest` with the `r_hat` flag set, will calculate $\hat{R}$ for each stochastic node in the trace.

```python
az.summary(pku_trace)
```

### Exercise

Clearly the model above has not yet converged (we only ran it for 100 iterations without tuning, after all). Try running the `pku_model` for a larger number of iterations, and see when $\hat{R}$ converges to 1.0.

```python
# Write your answer here
```

## Effective Sample Size

In general, samples drawn from MCMC algorithms will be autocorrelated. Unless the autocorrelation is very severe, this is not a big deal, other than the fact that autocorrelated chains may require longer sampling in order to adequately characterize posterior quantities of interest. The calculation of autocorrelation is performed for each lag $i=1,2,\ldots,k$ (the correlation at lag 0 is, of course, 1) by:

$$\hat{\rho}_i = 1 - \frac{V_i}{2\hat{\text{Var}}(\theta | y)}$$

where $\hat{\text{Var}}(\theta | y)$ is the same estimated variance as calculated for the Gelman-Rubin statistic, and $V_i$ is the variogram at lag $i$ for $\theta$:

$$\text{V}_i = \frac{1}{m(n-i)}\sum_{j=1}^m \sum_{k=i+1}^n (\theta_{jk} - \theta_{j(k-i)})^2$$

This autocorrelation can be visualized using the `plot_autocorr` function in ArViZ:

```python
az.plot_autocorr(pku_trace, var_names=['σ_iq', 'μ_int'], combined=True);
```

You can see very severe autocorrelation in `μ_int`, which is not surprising given the trace that we observed earlier.

The amount of correlation in an MCMC sample influences the **effective sample size** (ESS) of the sample. The ESS estimates how many *independent* draws contain the same amount of information as the *dependent* sample obtained by MCMC sampling.

Given a series of samples $x_j$, the empirical mean is

$$
\hat{\mu} = \frac{1}{n}\sum_{j=1}^n x_j
$$

and the variance of the estimate of the empirical mean is

$$
\operatorname{Var}(\hat{\mu}) = \frac{\sigma^2}{n},
$$
where $\sigma^2$ is the true variance of the underlying distribution.

Then the effective sample size is defined as the denominator that makes this relationship still be true:

$$
\operatorname{Var}(\hat{\mu}) = \frac{\sigma^2}{n_{\text{eff}}}.
$$

The effective sample size is estimated using the partial sum:

$$\hat{n}_{eff} = \frac{mn}{1 + 2\sum_{i=1}^T \hat{\rho}_i}$$

where $T$ is the first odd integer such that $\hat{\rho}_{T+1} + \hat{\rho}_{T+2}$ is negative.

The issue here is related to the fact that we are **estimating** the effective sample size from the fit output. Values of $n_{eff} / n_{iter} < 0.001$ indicate a biased estimator, resulting in an overestimate of the true effective sample size.

Vehtari *et al* (2019) recommend an ESS of at least 400 to ensure reliable estimates of variances and autocorrelations. They also suggest running at least 4 chains before calculating any diagnostics.

Its important to note that ESS can vary across the quantiles of the MCMC chain being sampled.

```python
az.plot_ess(pku_trace, var_names=['μ_int']);
```

Using ArViZ, we can visualize the evolution of ESS as the MCMC sample accumulates. When the model is converging properly, both lines in this plot should be approximately linear.

The standard ESS estimate, which mainly assesses how well the centre of the distribution is resolved, is referred to as **bulk-ESS**. In order to estimate intervals reliably, it is also important to consider the **tail-ESS**.

```python
az.plot_ess(pku_trace, var_names=['μ_int'], kind='evolution');
```

ESS statistics can also be tabulated, by generating a `summary` of the parameters of interest.

```python
az.summary(pku_trace)
```

It is tempting to want to **thin** the chain to eliminate the autocorrelation (*e.g.* taking every 20th sample from traces with autocorrelation as high as 20), but this is a waste of time. Since thinning deliberately throws out the majority of the samples, no efficiency is gained; you ultimately require more samples to achive a particular desired sample size.

## Bayesian Fraction of Missing Information

The Bayesian fraction of missing information (BFMI) is a measure of how hard it is to
sample level sets of the posterior at each iteration. Specifically, it quantifies **how well momentum resampling matches the marginal energy distribution**.

$$\text{BFMI} = \frac{\mathbb{E}_{\pi}[\text{Var}_{\pi_{E|q}}(E|q)]}{\text{Var}_{\pi_{E}}(E)}$$

$$\widehat{\text{BFMI}} = \frac{\sum_{i=1}^N (E_n - E_{n-1})^2}{\sum_{i=1}^N (E_n - \bar{E})^2}$$

A small value indicates that the adaptation phase of the sampler was unsuccessful, and invoking the central limit theorem may not be valid. It indicates whether the sampler is able to *efficiently* explore the posterior distribution.

Though there is not an established rule of thumb for an adequate threshold, values close to one are optimal. Reparameterizing the model is sometimes helpful for improving this statistic.

BFMI calculation is only available in samples that were simulated using HMC or NUTS.

```python
az.bfmi(pku_trace)
```

```python
with pku_model:

    pku_trace = pm.sample(return_inferencedata=True, random_seed=RANDOM_SEED)
```

```python
az.bfmi(pku_trace)
```

Another way of diagnosting this phenomenon is by comparing the overall distribution of
energy levels with the *change* of energy between successive samples. Ideally, they should be very similar.

If the distribution of energy transitions is narrow relative to the marginal energy distribution, this is a sign of inefficient sampling, as many transitions are required to completely explore the posterior. On the other hand, if the energy transition distribution is similar to that of the marginal energy, this is evidence of efficient sampling, resulting in near-independent samples from the posterior.

```python
az.plot_energy(pku_trace);
```

## Goodness of Fit

As noted at the beginning of this section, convergence diagnostics are only the first step in the evaluation
of MCMC model outputs. It is possible for an entirely unsuitable model to converge, so additional steps are needed to ensure that the estimated model adequately fits the data.

One intuitive way of evaluating model fit is to compare model predictions with the observations used to fit
the model. In other words, the fitted model can be used to simulate data, and the distribution of the simulated data should resemble the distribution of the actual data.

Fortunately, simulating data from the model is a natural component of the Bayesian modelling framework. Recall, from the discussion on prediction, the posterior predictive distribution:

$$p(\tilde{y}|y) = \int p(\tilde{y}|\theta) f(\theta|y) d\theta$$

Here, $\tilde{y}$ represents some hypothetical new data that would be expected, taking into account the posterior uncertainty in the model parameters.

Sampling from the posterior predictive distribution is easy in PyMC3. The `sample_posterior_predictive` function draws posterior predictive samples from all of the observed variables in the model. Consider the PKU model,
where IQ is modeled as a Gaussian random variable, which is thought to be influenced by blood Phe levels.

The posterior predictive distribution of IQ uses the same functional form as the data likelihood, in this case a normal stochastic. Here is the corresponding sample from the posterior predictive distribution (we typically need very few samples relative to the MCMC sample):

```python
with pku_model:
    pku_ppc = pm.sample_posterior_predictive(pku_trace.posterior, samples=500)
```

The degree to which simulated data correspond to observations can be evaluated visually. This allows for a qualitative comparison of model-based replicates and observations. If there is poor fit, the true value of the data may appear in the tails of the histogram of replicated data, while a good fit will tend to show the true data in high-probability regions of the posterior predictive distribution.

```python
fig, axes = plt.subplots(10, 4, figsize=(12,15), sharex=True, sharey=True)

for ax in axes.ravel():
    i = np.random.randint(0, pku_ppc['iq_like'].shape[1])
    ax.hist(pku_ppc['iq_like'][:, i], alpha=0.3)
    ax.vlines(iq[i], 0, 100)

plt.tight_layout();
```

A quantitative approach is to calculate quantiles of each observed data point relative to the corresponding distribution of posterior-simulated values. For an adequate fit, there should not be severe peaks in the histogram near zero and one.

```python
from scipy.stats import percentileofscore

plt.hist([np.round(percentileofscore(x, y)/100, 2) for x,y in zip(pku_ppc['iq_like'], iq)], bins=25);
```

---

## References

Gelman, A., & Rubin, D. B. (1992). Inference from iterative simulation using multiple sequences. Statistical Science. A Review Journal of the Institute of Mathematical Statistics, 457–472.

[Vehtari, Gelman, Simpson, Carpenter, Bürkner (2019)](https://arxiv.org/abs/1903.08008) Rank-normalization, folding, and localization: An improved $\hat{R}$ for assessing convergence of MCMC


# 状態方程式の導出
倒立振子のダイナミクスをラグランジュの運動方程式から導出する．

```python
import sympy as sy
```

## 必要なシンボリック文字を用意

```python
t = sy.Symbol("t")  # 時刻
M = sy.Symbol("M")  # 台車の質量
m = sy.Symbol("m")  # 振り子の質量
l = sy.Symbol("l")  # 長さ
L = 2*l
g = sy.Symbol("g")  # 重力加速度
D_theta, D_x = sy.symbols("D_theta, D_x")  # 回転方向と並進方向の摩擦係数

x = sy.Function("x")  # 台車の位置
theta = sy.Function("theta")  # 振り子の角度
u = sy.Symbol("u")  # 制御入力
```

## エネルギー
運動エネルギー，ポテンシャルエネルギー，損失エネルギーは次である．

運動エネルギーを定義

```python
K = sy.Rational(2, 3)*m*l**2*sy.Derivative(theta(t), t)**2 +\
    m*l*sy.Derivative(x(t), t)*sy.Derivative(theta(t), t)*sy.cos(theta(t)) +\
        sy.Rational(1, 2)*(M+m)*sy.Derivative(x(t), t)**2
K
```

$\displaystyle \frac{2 l^{2} m \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2}}{3} + l m \cos{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} \frac{d}{d t} x{\left(t \right)} + \left(\frac{M}{2} + \frac{m}{2}\right) \left(\frac{d}{d t} x{\left(t \right)}\right)^{2}$

ポテンシャルエネルギーを定義

```python
U = m*g*l*sy.cos(theta(t))
U
```

$\displaystyle g l m \cos{\left(\theta{\left(t \right)} \right)}$

損失エネルギーを定義

```python
D = sy.Rational(1, 2)*D_theta*sy.Derivative(theta(t), t)**2 + sy.Rational(1, 2)*D_x*sy.Derivative(x(t), t)**2
D
```

$\displaystyle \frac{D_{\theta} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2}}{2} + \frac{D_{x} \left(\frac{d}{d t} x{\left(t \right)}\right)^{2}}{2}$

## ラグランジュの運動方程式に代入
ラグランジュの運動方程式は次式．

### x方向
運動エネルギーの速度偏微分の時間微分

```python
dKdv = sy.diff(K, sy.Derivative(x(t), t))
dKdv_dot = sy.diff(dKdv, t)
dKdv_dot
```

$\displaystyle - l m \sin{\left(\theta{\left(t \right)} \right)} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} + l m \cos{\left(\theta{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} + \left(M + m\right) \frac{d^{2}}{d t^{2}} x{\left(t \right)}$

運動エネルギーの位置偏微分

```python
dKdx = sy.diff(K, x(t))
dKdx
```

$\displaystyle 0$

ポテンシャルエネルギーの位置偏微分

```python
dUdx = sy.diff(U, x(t))
dUdx
```

$\displaystyle 0$

損失エネルギーの速度偏微分

```python
dDdv = sy.diff(D, sy.Derivative(x(t), t))
dDdv
```

$\displaystyle D_{x} \frac{d}{d t} x{\left(t \right)}$

x方向の入力

```python
u_x = u
u_x
```

$\displaystyle u$

### theta方向
運動エネルギーの角速度偏微分の時間微分

```python
dKdomega = sy.diff(K, sy.Derivative(theta(t), t))
dKdomega_dot = sy.diff(dKdomega, t)
dKdomega_dot
```

$\displaystyle \frac{4 l^{2} m \frac{d^{2}}{d t^{2}} \theta{\left(t \right)}}{3} - l m \sin{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} \frac{d}{d t} x{\left(t \right)} + l m \cos{\left(\theta{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} x{\left(t \right)}$

運動エネルギーの角度偏微分

```python
dKdtheta = sy.diff(K, theta(t))
dKdtheta
```

$\displaystyle - l m \sin{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} \frac{d}{d t} x{\left(t \right)}$

ポテンシャルエネルギーの角度偏微分

```python
dUdtheta = sy.diff(U, theta(t))
dUdtheta
```

$\displaystyle - g l m \sin{\left(\theta{\left(t \right)} \right)}$

損失エネルギーの角速度偏微分

```python
dDdomega = sy.diff(D, sy.Derivative(theta(t), t))
dDdomega
```

$\displaystyle D_{\theta} \frac{d}{d t} \theta{\left(t \right)}$

回転方向の入力

```python
u_theta = 0
u_theta
```

    0

## 加速度を求める
これまでの演算から運動方程式が得られた．
x方向の運動方程式

```python
f_x = dKdv_dot - dKdx + dUdx + dDdv - u_x
f_x
```

$\displaystyle D_{x} \frac{d}{d t} x{\left(t \right)} - l m \sin{\left(\theta{\left(t \right)} \right)} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} + l m \cos{\left(\theta{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} - u + \left(M + m\right) \frac{d^{2}}{d t^{2}} x{\left(t \right)}$

theta方向の運動方程式

```python
f_theta = dKdomega_dot - dKdtheta + dUdtheta + dDdomega - u_theta
f_theta
```

$\displaystyle D_{\theta} \frac{d}{d t} \theta{\left(t \right)} - g l m \sin{\left(\theta{\left(t \right)} \right)} + \frac{4 l^{2} m \frac{d^{2}}{d t^{2}} \theta{\left(t \right)}}{3} + l m \cos{\left(\theta{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} x{\left(t \right)}$

運動方程式は加速度$\ddot{x}$と角加速度$\ddot{\theta}$の連立方程式になっているため，`sy.solve`を使って加速度を求める．

```python
sol = sy.solve(
    [f_x, f_theta],
    [sy.Derivative(sy.Derivative(x(t), t), t), sy.Derivative(sy.Derivative(theta(t), t), t)]
)
for k in sol.keys():
    sol[k] = sol[k].simplify()
```

求まった加速度は以下の通り．

```python
a_x = sol[sy.Derivative(x(t), (t, 2))]
a_x
```

$\displaystyle \frac{3 D_{\theta} \cos{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} - 4 D_{x} l \frac{d}{d t} x{\left(t \right)} - \frac{3 g l m \sin{\left(2 \theta{\left(t \right)} \right)}}{2} + 4 l^{2} m \sin{\left(\theta{\left(t \right)} \right)} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} + 4 l u}{l \left(4 M + 3 m \sin^{2}{\left(\theta{\left(t \right)} \right)} + m\right)}$

```python
a_theta = sol[sy.Derivative(theta(t), (t, 2))]
a_theta
```

$\displaystyle \frac{3 \left(- D_{\theta} M \frac{d}{d t} \theta{\left(t \right)} - D_{\theta} m \frac{d}{d t} \theta{\left(t \right)} + D_{x} l m \cos{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} x{\left(t \right)} + M g l m \sin{\left(\theta{\left(t \right)} \right)} + g l m^{2} \sin{\left(\theta{\left(t \right)} \right)} - \frac{l^{2} m^{2} \sin{\left(2 \theta{\left(t \right)} \right)} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2}}{2} - l m u \cos{\left(\theta{\left(t \right)} \right)}\right)}{l^{2} m \left(4 M + 3 m \sin^{2}{\left(\theta{\left(t \right)} \right)} + m\right)}$

## 状態変数
次のように状態変数をおく．

```python
X = sy.Matrix(sy.MatrixSymbol('x', 4, 1))
X
```

$\displaystyle \left[\begin{array}{c}x_{0, 0}\\x_{1, 0}\\x_{2, 0}\\x_{3, 0}\end{array}\right]$

状態変数を運動方程式に代入

```python
x1_dot = X[1, 0]
x3_dot = X[3, 0]

x2_dot = a_x.subs([
    (sy.Derivative(x(t), t), X[1, 0]),
    (sy.Derivative(theta(t), t), X[3, 0]),
    (x(t), X[0, 0]),
    (theta(t), X[2, 0])
])

x4_dot = a_theta.subs([
    (sy.Derivative(x(t), t), X[1, 0]),
    (sy.Derivative(theta(t), t), X[3, 0]),
    (x(t), X[0, 0]),
    (theta(t), X[2, 0])
])

Fxu = sy.Matrix([
    [x1_dot],
    [x2_dot],
    [x3_dot],
    [x4_dot],
])
Fxu
```

$\displaystyle \left[\begin{matrix}x_{1, 0}\\\frac{3 D_{\theta} \cos{\left(x_{2, 0} \right)} x_{3, 0} - 4 D_{x} l x_{1, 0} - \frac{3 g l m \sin{\left(2 x_{2, 0} \right)}}{2} + 4 l^{2} m \sin{\left(x_{2, 0} \right)} x_{3, 0}^{2} + 4 l u}{l \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)}\\x_{3, 0}\\\frac{3 \left(- D_{\theta} M x_{3, 0} - D_{\theta} m x_{3, 0} + D_{x} l m \cos{\left(x_{2, 0} \right)} x_{1, 0} + M g l m \sin{\left(x_{2, 0} \right)} + g l m^{2} \sin{\left(x_{2, 0} \right)} - \frac{l^{2} m^{2} \sin{\left(2 x_{2, 0} \right)} x_{3, 0}^{2}}{2} - l m u \cos{\left(x_{2, 0} \right)}\right)}{l^{2} m \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)}\end{matrix}\right]$

状態方程式が求まった．

****
# 状態方程式の線形化
まず状態方程式を入力アフィンの形にする

```python
fx = Fxu.subs(u, 0)
fx
```

$\displaystyle \left[\begin{matrix}x_{1, 0}\\\frac{3 D_{\theta} \cos{\left(x_{2, 0} \right)} x_{3, 0} - 4 D_{x} l x_{1, 0} - \frac{3 g l m \sin{\left(2 x_{2, 0} \right)}}{2} + 4 l^{2} m \sin{\left(x_{2, 0} \right)} x_{3, 0}^{2}}{l \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)}\\x_{3, 0}\\\frac{3 \left(- D_{\theta} M x_{3, 0} - D_{\theta} m x_{3, 0} + D_{x} l m \cos{\left(x_{2, 0} \right)} x_{1, 0} + M g l m \sin{\left(x_{2, 0} \right)} + g l m^{2} \sin{\left(x_{2, 0} \right)} - \frac{l^{2} m^{2} \sin{\left(2 x_{2, 0} \right)} x_{3, 0}^{2}}{2}\right)}{l^{2} m \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)}\end{matrix}\right]$

```python
Fxu = Fxu.expand()
B = sy.Matrix([
    [Fxu[0,0].coeff(u, 1)],
    [Fxu[1,0].coeff(u, 1)],
    [Fxu[2,0].coeff(u, 1)],
    [Fxu[3,0].coeff(u, 1)],
])
B
```

$\displaystyle \left[\begin{matrix}0\\\frac{4 l}{4 M l + 3 l m \sin^{2}{\left(x_{2, 0} \right)} + l m}\\0\\- \frac{3 l m \cos{\left(x_{2, 0} \right)}}{4 M l^{2} m + 3 l^{2} m^{2} \sin^{2}{\left(x_{2, 0} \right)} + l^{2} m^{2}}\end{matrix}\right]$

fxを状態ベクトルで偏微分し，x'近傍で線形化する

```python
A = fx.jacobian(X)
A
```

$\displaystyle \left[\begin{matrix}0 & 1 & 0 & 0\\0 & - \frac{4 D_{x}}{4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m} & - \frac{6 m \left(3 D_{\theta} \cos{\left(x_{2, 0} \right)} x_{3, 0} - 4 D_{x} l x_{1, 0} - \frac{3 g l m \sin{\left(2 x_{2, 0} \right)}}{2} + 4 l^{2} m \sin{\left(x_{2, 0} \right)} x_{3, 0}^{2}\right) \sin{\left(x_{2, 0} \right)} \cos{\left(x_{2, 0} \right)}}{l \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)^{2}} + \frac{- 3 D_{\theta} \sin{\left(x_{2, 0} \right)} x_{3, 0} - 3 g l m \cos{\left(2 x_{2, 0} \right)} + 4 l^{2} m \cos{\left(x_{2, 0} \right)} x_{3, 0}^{2}}{l \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)} & \frac{3 D_{\theta} \cos{\left(x_{2, 0} \right)} + 8 l^{2} m \sin{\left(x_{2, 0} \right)} x_{3, 0}}{l \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)}\\0 & 0 & 0 & 1\\0 & \frac{3 D_{x} \cos{\left(x_{2, 0} \right)}}{l \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)} & - \frac{18 \left(- D_{\theta} M x_{3, 0} - D_{\theta} m x_{3, 0} + D_{x} l m \cos{\left(x_{2, 0} \right)} x_{1, 0} + M g l m \sin{\left(x_{2, 0} \right)} + g l m^{2} \sin{\left(x_{2, 0} \right)} - \frac{l^{2} m^{2} \sin{\left(2 x_{2, 0} \right)} x_{3, 0}^{2}}{2}\right) \sin{\left(x_{2, 0} \right)} \cos{\left(x_{2, 0} \right)}}{l^{2} \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)^{2}} + \frac{3 \left(- D_{x} l m \sin{\left(x_{2, 0} \right)} x_{1, 0} + M g l m \cos{\left(x_{2, 0} \right)} + g l m^{2} \cos{\left(x_{2, 0} \right)} - l^{2} m^{2} \cos{\left(2 x_{2, 0} \right)} x_{3, 0}^{2}\right)}{l^{2} m \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)} & \frac{3 \left(- D_{\theta} M - D_{\theta} m - l^{2} m^{2} \sin{\left(2 x_{2, 0} \right)} x_{3, 0}\right)}{l^{2} m \left(4 M + 3 m \sin^{2}{\left(x_{2, 0} \right)} + m\right)}\end{matrix}\right]$

****
# ファイルに出力
求めた状態方程式をファイルに書き出す．
## py出力
pythonのコード

```python
from sympy.printing.numpy import NumPyPrinter

with open("state_eq.py", "w") as f:
    numpy_word = "import numpy\n\ndef f(x, u, m, M, l, g, D_x, D_theta):\n    return "
    f.write(numpy_word)
    f.write(NumPyPrinter().doprint(Fxu))

with open("A.py", "w") as f:
    numpy_word = "import numpy\n\ndef f(x, m, M, l, g, D_x, D_theta):\n    return "
    f.write(numpy_word)
    f.write(NumPyPrinter().doprint(A))

with open("B.py", "w") as f:
    numpy_word = "import numpy\n\ndef f(x, m, M, l, g, D_x, D_theta):\n    return "
    f.write(numpy_word)
    f.write(NumPyPrinter().doprint(B))
```

## Cコード出力
C言語のコードを作成．高速化したいときに使うと良い．

```python
from sympy.utilities.codegen import codegen

codegen(
    name_expr = ("state_eq", Fxu),
    language = "C",
    project= "state_eq",
    to_files = True
)

codegen(
    name_expr = ("A", A),
    language = "C",
    project= "A",
    to_files = True
)
codegen(
    name_expr = ("B", B),
    language = "C",
    project= "B",
    to_files = True
)
```

## julia
juliaのコード出力機能もある．

```python
from sympy import julia_code
with open("ju.txt", "w") as f:
    f.write(julia_code(Fxu))
    f.write("\n")
    f.write(julia_code(A))
    f.write("\n")
    f.write(julia_code(B))
```

sympyではfortranやc++のコードを出力することもできる．
codeprinterで検索すると情報がある．