```python
import numpy as np
import sympy as sy
from sympy.utilities.codegen import codegen
import control.matlab as cm
import re
import matplotlib.pyplot as plt
from scipy import signal
```

```python
z = sy.symbols('z', real=False)
r1,s0,s1,s2 = sy.symbols('r1,s0,s1,s2', real=True)
hh,a = sy.symbols('h,a', real=True, positive=True)

Bp = sy.poly(z+0.7, z)
Ap = sy.poly( z**2 - 1.8*z + 0.81, z)
Ac = sy.poly( z**2 - 1.5*z + 0.7, z)
Ao = sy.poly((z-a)**2, z)
Acl = Ac*Ao

Rp = sy.poly(z+r1, z)
Sp = sy.poly(s0*z**2 + s1*z + s2, z)
dioph=(Ap*Rp*(z-1)+Bp*Sp-Acl).all_coeffs()
print dioph
print Acl
print Ap*Rp
print Ac
print Ap*Rp
print Ap*Rp + Bp*Sp
```

    [2.0*a + 1.0*r1 + 1.0*s0 - 1.3, -1.0*a**2 - 3.0*a - 2.8*r1 + 0.7*s0 + 1.0*s1 + 1.91, 1.5*a**2 + 1.4*a + 2.61*r1 + 0.7*s1 + 1.0*s2 - 0.81, -0.7*a**2 - 0.81*r1 + 0.7*s2]
    Poly(1.0*z**4 + (-2.0*a - 1.5)*z**3 + (1.0*a**2 + 3.0*a + 0.7)*z**2 + (-1.5*a**2 - 1.4*a)*z + 0.7*a**2, z, domain='RR[a]')
    Poly(1.0*z**3 + (1.0*r1 - 1.8)*z**2 + (-1.8*r1 + 0.81)*z + 0.81*r1, z, domain='RR[r1]')
    Poly(1.0*z**2 - 1.5*z + 0.7, z, domain='RR')
    Poly(1.0*z**3 + (1.0*r1 - 1.8)*z**2 + (-1.8*r1 + 0.81)*z + 0.81*r1, z, domain='RR[r1]')
    Poly((1.0*s0 + 1.0)*z**3 + (1.0*r1 + 0.7*s0 + 1.0*s1 - 1.8)*z**2 + (-1.8*r1 + 0.7*s1 + 1.0*s2 + 0.81)*z + 0.81*r1 + 0.7*s2, z, domain='RR[r1,s0,s1,s2]')

```python
sol = sy.solve(dioph, (r1,s0,s1,s2))
print 'r_1 = %f' % sol[r1]
print 's_0 = %f' % sol[s0]
print 's_1 = %f' % sol[s1]
print 's_1 = %f' % sol[s1]

t0 = Ac.evalf(subs={z:1})/Bp.evalf(subs={z:1,})
print 't_0 = %f' % t0

R = Rp.subs(sol)
S = Sp.subs(sol)
T = t0*Ao

Hc = T*Bp/(Ac*Ao)
Hcc = t0*0.8/Ac
sy.pretty_print(sy.expand(Hc))
sy.pretty_print(sy.expand(Hcc))
sy.pretty_print(Hc.evalf(subs={z:1}))

sy.pretty_print(sy.simplify(Ap*R + Bp*S))
```

    r_1 = 0.077206
    s_0 = 0.422794
    s_1 = -0.629779
    s_1 = -0.629779
    t_0 = 0.117647
    0.117647058823529⋅Poly(1.0*z + 0.7, z, domain='RR')⋅Poly(1.0*z**2 - 0.8*z + 0.
    ──────────────────────────────────────────────────────────────────────────────
                  Poly(1.0*z**4 - 2.3*z**3 + 2.06*z**2 - 0.8*z + 0.112, z, domain=

    16, z, domain='RR')
    ───────────────────
    'RR')
                 0.0941176470588235
    ────────────────────────────────────────────
    Poly(1.0*z**2 - 1.5*z + 0.7, z, domain='RR')
    0.999999999999991
    Poly(1.42279411764706*z**3 - 2.05661764705882*z**2 + 0.47952205882353*z + 0.23
    7073529411765, z, domain='RR')

```python

```

```python
1.0/0.3125
```

    3.2

```python
num = sy.list2numpy((Ap*R).all_coeffs(), dtype=np.float64)
den = sy.list2numpy((Ac*Ao).all_coeffs(), dtype=np.float64)
print num
print den
print type(num[0])
#Hd = cm.tf(num[:-1], den[:-1], -1)
Hd = cm.tf([1], [1, 0.5])
print Hd
ystep, t = cm.step(Hd, np.arange(30))
plt.figure()
plt.plot(t, ystep)
plt.show()
```

    [ 1.   -0.7  -0.08  0.  ]
    [ 1.  -0.7  0.   0. ]
    <type 'numpy.float64'>

       1
    -------
    s + 0.5

```python
# Reorganize solution expression for matlab code generation
sol_expr = ('RST_DC_lab', [Bp.all_coeffs()[0], Bp.all_coeffs()[1],
                           Ap.all_coeffs()[1], Ap.all_coeffs()[2],
                           sol[r1], sol[s0], sol[s1], A2p.subs(z, 1)/Bp.subs(z,1), h,np.exp(h*po1) ])
```

```python
# Export to matlab code
[(m_name, m_code)] = codegen(sol_expr, 'octave')
```

```python
m_code = m_code.replace("out1", "b0").replace("out2", "b1").replace("out3", "a1").replace("out4", "a2")
m_code = m_code.replace("out5", "r1").replace("out6", "s0").replace("out7", "s1").replace("out8", "t0")
m_code = m_code.replace("out9", "h").replace("out10", "obsPole")
m_code = m_code.replace("function ", "% function ")
m_code = m_code.replace("end", "")
print m_code
with open("/home/kjartan/Dropbox/undervisning/tec/MR2007/labs/dc_rst_design.m", "w") as text_file:
    text_file.write(m_code)
```

    % function [b0, b1, a1, a2, r1, s0, s1, t0, h, b00] = RST_DC_lab()
      %RST_DC_LAB  Autogenerated by sympy
      %   Code generated with sympy 0.7.6.1
      %
      %   See http://www.sympy.org/ for more information.
      %
      %   This file is part of 'project'

      b0 = 0.0905485642505141;
      b1 = 0.0860565404440641;
      a1 = -1.85841144421398;
      a2 = 0.858411444213975;
      r1 = -0.632898354797954;
      s0 = 0.37929607879435;
      s1 = -0.324459627611956;
      t0 = 0.37087688639506;
      h = 0.04;
      b00 = 0.852143788966211;

```python
cm.step?
```

```python

G = Km * cm.tf([1], [tau, 1, 0])
Gd = Km * cm.tf([tau*(hpt-1+np.exp(-hpt)), tau*(1-(1+hpt)*np.exp(-hpt))], [1, -(1+np.exp(-hpt)), np.exp(-hpt)], h)
Gd2 = cm.c2d(G, h)
print Gd
print Gd2
```

      0.09055 z + 0.08606
    ----------------------
    z^2 - 1.858 z + 0.8584

    dt = 0.04

      0.09055 z + 0.08606
    ----------------------
    z^2 - 1.858 z + 0.8584

    dt = 0.04

```python
print A2p
print A2p.evalf(subs={z:1})
print Bp
print Bp.evalf(subs={z:1})

```

    Poly(z**2 - 1.58555290309441*z - (-0.792776451547206 + 0.168974310731771*I)*(0.792776451547206 + 0.168974310731771*I), z, domain='EX')
    0.0714939167206446
    Poly(0.00133942860759726*z + 0.00124712240506047, z, domain='RR')
    0.00258655101265772

```python
0.3/(5*np.sqrt(2))
```

    0.042426406871192847

```python
np.exp(-0.21)*np.sin(0.21)
```

    0.16897431073177133

```python
np.exp(0.03*(-14))
```

    0.65704681981505675

```python
 0.746*41.8

```

    31.182799999999997

```python

```


### LaTeX 数学公式 (关键抄例子改改用)

- 怎么在 `Jupyter 的 markdown 支持 latex 数学公式` 自行搜索

#### 上标 和 下标
- 上标
    - `$x^2$` 表示 $$x^2$$
    - `$e^2x$` 表示 $$e^2x$$
    - `$e^{2x}$` 表示 $$e^{2x}$$

- 下标
    - `$x_i$` 表示 $$x_i$$
    - `$_{10}C_5$` 表示 $$_{10}C_5$$

#### 命令

- 开方
    - `$ \sqrt{2\pi} $` 表示 $$ \sqrt{2\pi} $$
    - `$\sqrt[n]{a*b^3}$` 表示 $$\sqrt[n]{a*b^3}$$
    - `$\sqrt[3]{a}$` 表示 $$\sqrt[3]{a}$$

- 分数
    - `$y = \frac{a}{m*(b+c)}$` 表示 $$y = \frac{a}{m*(b+c)}$$
    - `$y = \frac{a}{m \times} (b+c)}$` 表示 $$y = \frac{a}{m \times (b+c)}$$

#### 符号

- 符号也是以反斜杠开头，反斜杠之后跟符号名。在LaTeX存在大量的符号：
    - 希腊字母如 \alpha, \beta, \gamma :
        - `$\alpha$` 表示 $$\alpha$$
        - `$\beta$` 表示 $$\beta$$
        - `$\gamma$` 表示 $$\gamma$$

    - 大写希腊字母，如 \Phi, \Lambda, \Gamma :
        - `$\Phi$` 表示 $$\Phi$$
        - `$\Lambda$` 表示 $$\Lambda$$
        - `$\Gamma$` 表示 $$\Gamma$$

**注意:** beta的大写形式是 B ，而不是 \Beta 。

- 比如乘、正负、并集、卷积符号:
    - `$\times$` 表示 $$\times$$
    - `$\pm$` 表示 $$\pm$$
    - `$\cup$` 表示 $$\cup$$
    - `$\cap$` 表示 $$\cap$$
    - `$\oplus$` 表示 $$\oplus$$

- 三角函数像
    - \sin, \cosh, \arctan 表示 sin、cosh、arctan

- 关系运算, 例如:

| 输入 | 显示 | 输入 | 显示 |
| :--- | :--- | :--- | :--- |
| `$\leq$` | $$\leq$$ | `$\ll$` | $$\ll$$ |
| `$\geq$` | $$\geq$$ | `$\gg$` | $$\gg$$ |
| `$\approx$` | $$\approx$$ | `$\neq$` | $$\neq$$ |
| `$\equiv$` | $$\equiv$$ | `$\sim$` | $$\sim$$ |
| `$\simeq$` | $$\simeq$$ | `$\approx$` | $$\approx$$ |
| `$\subset$` | $$\subset$$ | `$\supset$` | $$\supset$$ |
| `$\subseteq$` | $$\subseteq$$ | `$\supseteq$` | $$\supseteq$$ |
| `$\in$` | $$\in$$ | `$\ni$` | $$\ni$$ |
| `$\ne$` | $$\ne$$ | `$\notin$` | $$\notin$$ |
| `$\div$` | $$\div$$ | `$\cdot$` | $$\cdot$$ |
| `$\oplus$` | $$\oplus$$ | `$\bigoplus$` | $$\bigoplus$$ |
| `$\otimes$` | $$\otimes$$ | `$\bigotimes$` | $$\bigotimes$$ |
| `$\lor$` | $$\lor$$ | `$\land$` | $$\land$$ |

- 点符号, 例如：
    - `$\cdots$` 表示 $$\cdots$$
    - `$\ldots$` 表示 $$\ldots$$
    - `$\ddots$` 表示 $$\ddots$$

- 其他的变量符，如：
    - `$\infty$` 表示 $$\infty$$
    - `$\nabla$` 表示 $$\nabla$$
    - `$\Delta$` 表示 $$\Delta$$
    - `$\partial$` 表示 $$\partial$$
    - `$\Sigma$` 表示 $$\Sigma$$
    - `$\Theta$` 表示 $$\Theta$$
    - `$\Omega$` 表示 $$\Omega$$
    - `$\phi$` 表示 $$\phi$$
    - `$\Phi$` 表示 $$\Phi$$

#### 口音

- `$\hat x$` 表示 $$\hat x$$
- `$\widehat{abs}` 表示 $$\widehat{abs}$$
- 平均
    - `$\bar x$` 表示 $$\bar x$$
    - `$\overline{abs}$` 表示 $$\overline{abs}$$

- 导数通过点表示:
    - `$\dot x$` 表示 $$\dot x$$
    - `$\ddot x$` 表示 $$\ddot x$$

- 箭头(向量)为:
    - `$\vec{x}$` 表示 $$\vec{x}$$
    - `$\overrightarrow{AB}$` 表示 $$\overrightarrow{AB}$$
    - `$\overleftarrow{AB}$` 表示 $$\overleftarrow{AB}$$

#### 括号

- 圆括号
    - `$z=(\frac{dx}{dy})^{1/3}$` 表示 $$z=(\frac{dx}{dy})^{1/3}$$
    - `$z=\left(\frac{dx}{dy}\right)^{1/3}$` 表示 $$z=\left(\frac{dx}{dy}\right)^{1/3}$$
    - `$z=\left(\frac{dx}{dy}\right)^{1/3}$` 表示 $$z=\left(\frac{dx}{dy}\right)^{1/3}$$

- 尖括号
    - `$ {\langle} {\phi} \mid {\psi} {\rangle} $` 表示 $$ {\langle} {\phi} \mid {\psi} {\rangle} $$

- 特殊的括号：
    - `$\vert` 表示 $$\vert$$
    - `$\mid$` 表示 $$\mid$$

- 矩阵括号
    - 方括号包围的矩阵 `$\left[\begin{matrix}a & b \cr c & d\end{matrix}\right]$` 表示 $$\left[\begin{matrix}a & b \cr c & d\end{matrix}\right]$$
    - 圆括号包围的矩阵, `$\left\lgroup\begin{matrix}a & b \cr c & d\end{matrix}\right\rgroup$` 表示 $$ \left\lgroup\begin{matrix}a & b \cr c & d\end{matrix}\right\rgroup$$

#### 字体及其选项
- 比如非斜体罗马文本
    - `$\textrm{abcdefghijklmn123456}$` 显示为 $$\textrm{abcdefghijklmn123456}$$
    - `$\rm{abcdefghijklmn123456}$` 显示为 $$\rm{abcdefghijklmn123456}$$

- 斜体字母
    - `$\mathit{abcdefghijklmn123456}$` 显示为 $$\mathit{abcdefghijklmn123456}$$

- Sans serif 字体
    - `$\mathsf{abcdefghijklmn123456}$` 显示为 $$\mathsf{abcdefghijklmn123456}$$

- Typerwriter 字体
    - `$\mathtt{abcdefghijklmn123456}$` 显示为 $$\mathtt{abcdefghijklmn123456}$$

- Calligraphic 字体
    - `$\mathcal{abcdefghijklmn123456}$` 显示为 $$\mathcal{abcdefghijklmn123456}$$

- Blackborad bold 字体
    - `$\mathbb{abcdefghijklmn123456}$` 显示为 $$\mathbb{abcdefghijklmn123456}$$

- Boldsymbol 字体加粗
    - `$ \boldsymbol{A\cdot x}=\lambda\cdot v $` 显示为 $$ \boldsymbol{A\cdot x}=\lambda\cdot v $$

#### 空格

在LaTeX中，符号之间的空格会被自动移除，所以 \int f(x) dx  显示为
$$ \int f(x) dx $$

- 通过 `\,` 或 `\:` 或 `\;` 添加空格， 其空格宽度分别为从小到大
    - `$ \int f(x) \, dx $` 显示为 $$ \int f(x) \, dx $$
    - `$ \int f(x) \: dx $` 显示为 $$ \int f(x) \: dx $$
    - `$ \int f(x) \; dx $` 显示为 $$ \int f(x) \; dx $$

- 更大的间隔，可以使用 \quad 和 \qquad 。第一个为 1em 的距离，第二个为 2em 的距离；
    - `$\int \quad f(x)$` 表示 $$\int \quad f(x)$$
    - `$\int \qquad f(x)$` 表示 $$\int \qquad f(x)$$

转义字符
```
如果你需要使用特殊的字符，比如 { 或 _ ，你必须通过反斜杠进行转义，例如 `\{ ，\_ ，\$ ` 分别表示 { 、 _ 、 $
```

#### 等式对齐

- 通过 `\\` 断开两个或多个等式，可实现等式中部对齐，例如：
`$ a_1=b_1+c_1 \\ a_2=b_2+c_2+d_2 \\ a_3=b_3+c_3 $ ` 表示

$$
a_1=b_1+c_1 \\
a_2=b_2+c_2+d_2 \\
a_3=b_3+c_3
$$

- 如果希望是左对齐，可以是：
`$ \begin{align} a_1&=b_1+c_1 \\ a_2&=b_2+c_2+d_2 \\ a_3&=b_3+c_3 \end{align} $` 表示

$ \begin{align} a_1&=b_1+c_1 \\ a_2&=b_2+c_2+d_2 \\ a_3&=b_3+c_3 \end{align} $

- 如果需要对方程进行编号，可以 使用 \tag ，例如：
`$ \begin{align} a_1&=b_1+c_1 \tag{公式1} \\ a_2&=b_2+c_2+d_2 \tag{公式2} \\ a_3&=b_3+c_3 \tag{公式3} \end{align} $`

$ \begin{align} a_1&=b_1+c_1 \tag{公式1}\\ a_2&=b_2+c_2+d_2 \tag{公式2}\\ a_3&=b_3+c_3 \tag{公式3} \end{align} $

- `$ \begin{align}\begin{cases} a_1&=b_1+c_1  \\ a_2&=b_2+c_2+d_2  \cr a_3&=b_3+c_3  \end{cases}\end{align} $` 表示

$
\begin{align}\begin{cases}
a_1&=b_1+c_1  \\ a_2&=b_2+c_2+d_2  \cr a_3&=b_3+c_3
\end{cases}\end{align}
$

#### 分段函数

- 例如需要些符号函数sign：
`$ sign(x)=\begin{cases} 1,&x>0 \\ 0,&x=0 \cr -1,&x<0 \end{cases} $`

$$ sign(x)=\begin{cases} 1,&x>0 \\ 0,&x=0 \cr -1,&x<0 \end{cases} $$

#### 向量

- `$\vec a$` 表示 $\vec a$
- `$\overrightarrow{AB}$` 表示 $\overrightarrow{AB}$
- `$overleftarrow{AB}$` 表示$\overleftarrow{AB}$

#### 积分运算

- `$\int_{0}^{\pi}{\tan x}$` 表示 $$\int_{0}^{\pi}{\tan x}$$

#### 求和运算

- `$\sum_{i=0}^{n}{i}$` 表示 $$\sum_{i=0}^{n}{i}$$

#### 乘积运算符(product operator)

- `$\prod_{i=1}^{9}{i}$` 表示 $$\prod_{i=1}^{9}{i}$$

#### 其它

- `$P(A \mid B) = \frac{ P(B \mid A) P(A) }{ P(B) }$` 表示 $$P(A \mid B) = \frac{ P(B \mid A) P(A) }{ P(B) }$$

- `$\overline{m+n}$` 表示 $$\overline{m+n}$$

- `$\underline{m+n}$` 表示 $$\underline{m+n}$$

- 上、下方给出一水平的大括号 命令 \overbrace 和 \underbrace 在表达式的上、下方给出一水平的大括号，例如

- `$\underbrace{a+b+\cdots+j}_{10}$` 表示
$$\underbrace{a+b+\cdots+j}_{10}$$

- `$\frac{-b\pm\sqrt{b^2-4 \times ac}}{2a}$` 表示

$$\frac{-b\pm\sqrt{b^2-4 \times a \times c}}{2 \times a}$$

- `$f(x_1,x_2,\ldots,x_n) = x_1^2 + x_2^2 + \cdots + x_n^2 $` 表示

$$f(x_1,x_x,\ldots,x_n) = x_1^2 + x_2^2 + \cdots + x_n^2 $$

- `$f(x,y,z) = 3y^2 z \left( 3 + \frac{7x+5}{1 + y^2} \right)$` 表示

$$ f(x,y,z) = 3y^2 z \left( 3 + \frac{7x+5}{1 + y^2} \right) $$

- `$\left. \frac{du}{dx} \right|_{x=0}.$` 表示 $$\left. \frac{du}{dx} \right|_{x=0}.$$

- `$ \begin{eqnarray} \cos 2\theta & = & \cos^2 \theta - \sin^2 \theta \\ & = & 2 \cos^2 \theta - 1 \end{eqnarray} $` 表示

$ \begin{eqnarray} \cos 2\theta & = & \cos^2 \theta - \sin^2 \theta \\  & = & 2 \cos^2 \theta - 1 \end{eqnarray} $

- `$\frac{du}{dt} \times \frac{d^2 u}{dx^2}$` 表示

$$ \frac{du}{dt} \times \frac{d^2 u}{dx^2} $$

- `$ \frac{\partial u}{\partial t} = h^2 \left( \frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2} + \frac{\partial^2 u}{\partial z^2}\right) $` 表示

$$ \frac{\partial u}{\partial t} = h^2 \left( \frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2} + \frac{\partial^2 u}{\partial z^2}\right) $$

- `$\lim_{x \to +\infty}, \inf_{x > s} \, 和 \, \sup_K$` 表示

$$ \lim_{x \to +\infty}, \inf_{x > s} \, 和 \, \sup_K $$

- `$\lim_{x \to 0} \frac{3x^2 +7x^3}{x^2 +5x^4} = 3$` 表示
$$ \lim_{x \to 0} \frac{3x^2 +7x^3}{x^2 +5x^4} = 3 $$

- `$ \sum_{k=1}^n k^2 = \frac{1}{2} n (n+1) $` 表示
$$ \sum_{k=1}^n k^2 = \frac{1}{2} n (n+1) $$

- `$\int_a^b f(x)\,dx$` 表示

$$\int_a^b f(x)\,dx$$

- ```
$
\begin{align}
\int_0^{+\infty} x^n e^{-x} \,dx &= n! \\
\int \cos \theta \,d\theta &= \sin \theta \\
\int_{x^2 + y^2 \leq R^2} f(x,y)\,dx\,dy &= \int_{\theta=0}^{2\pi} \int_{r=0}^Rf(r\cos\theta,r\sin\theta) r\,dr\,d\theta \\
\int_0^R \frac{2x\,dx}{1+x^2} &= \log(1+R^2)
\end{align}
$
``` 表示

$
\begin{align}
\int_0^{+\infty} x^n e^{-x} \,dx &= n! \\
\int \cos \theta \,d\theta &= \sin \theta \\
\int_{x^2 + y^2 \leq R^2} f(x,y)\,dx\,dy &= \int_{\theta=0}^{2\pi} \int_{r=0}^Rf(r\cos\theta,r\sin\theta) r\,dr\,d\theta \\
\int_0^R \frac{2x\,dx}{1+x^2} &= \log(1+R^2)
\end{align}
$

- `$\int_0^1 \int_0^1 x^2 y^2\,dx\,dy$` 表示 $$\int_0^1 \int_0^1 x^2 y^2\,dx\,dy$$

- `$\int \int_D f(x,y)\,dx\,dy$` 表示 $$\int \int_D f(x,y)\,dx\,dy$$

- `$\chi(\lambda) = \left| \begin{array}{ccc} \lambda - a & -b & -c \\ -d & \lambda - e & -f \\ -g & -h & \lambda - i \end{array} \right|$` 表示

$$
\chi(\lambda) = \left| \begin{array}{ccc}
\lambda - a & -b & -c \\
-d & \lambda - e & -f \\
-g & -h & \lambda - i \end{array} \right|
$$

```python

```


```python
from discretizer import Discretizer
from discretizer import momentum_operators
from discretizer import coordinates

import sympy
sympy.init_printing(use_latex='mathjax')
```

```python
kx, ky, kz = momentum_operators
x, y, z = coordinates
A, B, C = sympy.symbols('A B C', commutative=False)
```

# Before (won't work anymore)

```python
hamiltonian = kx*A*kx + ky*B*ky + B*kz**2 + C

space_dependent = {'A', 'B', 'C'}
discrete_coordinates = {'x', 'y'}
function_arguments = {'x', 'y'}

tb = Discretizer(hamiltonian, space_dependent, discrete_coordinates, function_arguments,
                 lattice_constant=2.0, verbose=True)
```

# Now

```python
hamiltonian = kx * A(x,y) * kx + ky * B(x,y) * ky + B(x,y) * kz**2 + C(x,y)
tb = Discretizer(hamiltonian, discrete_coordinates={'x','y'}, lattice_constant=2.0, verbose=True)
```

    Discrete coordinates set to:  ['x', 'y']

    Function generated for (0, 1):
    def _anonymous_func(site1, site2, p):
        (x, y, ) = site2.pos
        B = p.B
        return (-0.25*B(x, 1.0 + y))

    Function generated for (1, 0):
    def _anonymous_func(site1, site2, p):
        (x, y, ) = site2.pos
        A = p.A
        return (-0.25*A(1.0 + x, y))

    Function generated for (0, 0):
    def _anonymous_func(site, p):
        (x, y, ) = site.pos
        k_z = p.k_z
        C, B, A = p.C, p.B, p.A
        return (0.25*A(-1.0 + x, y) + 0.25*A(1.0 + x, y) + B(x, y)*k_z**2 + 0.25*B(x, -1.0 + y) + 0.25*B(x, 1.0 + y) + C(x, y))

# Note:
One can use ``subs`` method provided by sympy to simplify operation of substituting symbols with functions


# Import Problem Instance
We start by importing a simple problem instance to demonstrate the tsplib reader.

```python
from tsplib95 import tsplib95
import itertools
import networkx as nx

instance = tsplib95.load_problem('./tsplib/ulysses16.tsp')

instance.comment
```

    'Odyssey of Ulysses (Groetschel/Padberg)'

Remember, this repository contains a small selection of TSP instances that you can use to test your algorithms.

| name | nodes | description |
|------|-------|-------------|
| ulysses16.tsp | 16 | Odyssey of Ulysses |
| ulysses7.tsp | 7 | subset of ulysses16 for testing purposes |
| bayg29.tsp | 29 | 29 Cities in Bavaria |
| bier127.tsp | 127 | 127 Biergaerten in Augsburg |
| bier20.tsp | 20 | subset of bier127 |
| brazil58.tsp | 58 | 58 cities in Brazil |
| ali535.tsp | 535 | 535 Airports around the globe |
| d18512.tsp | 18512 | 18512 places in Germany |

The following calls show the dimension = number of nodes of the problem, its node set and the edge weights. The functions `instance.get_nodes()` and `instance.get_edges()` are implemented as iterators, so you can loop over the nodes or edges. To get a list of nodes or edges, you have to explicitly construct one using `list(instance.get_nodes())`. Note that node counting may start at 1 for some instances while others use 0 as starting point. For convenience, we store the index of the first node as `first_node`.

```python
instance.dimension

instance.get_nodes()
print("List of nodes: ", list(instance.get_nodes()))

first_node = min(instance.get_nodes())
first_node

for i,j in instance.get_edges():
    if i >= j:
        continue
    print(f"edge {{ {i:2},{j:2} }} has weight {instance.wfunc(i,j):3}.")

```

    List of nodes:  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
    edge {  1, 2 } has weight   6.
    edge {  1, 3 } has weight   5.
    edge {  1, 4 } has weight   3.
    edge {  1, 5 } has weight  11.
    edge {  1, 6 } has weight   8.
    edge {  1, 7 } has weight   7.
    edge {  1, 8 } has weight   1.
    edge {  1, 9 } has weight  12.
    edge {  1,10 } has weight   8.
    edge {  1,11 } has weight  26.
    edge {  1,12 } has weight   5.
    edge {  1,13 } has weight   5.
    edge {  1,14 } has weight   5.
    edge {  1,15 } has weight   7.
    edge {  1,16 } has weight   1.
    edge {  2, 3 } has weight   1.
    edge {  2, 4 } has weight   4.
    edge {  2, 5 } has weight  17.
    edge {  2, 6 } has weight  14.
    edge {  2, 7 } has weight  13.
    edge {  2, 8 } has weight   6.
    edge {  2, 9 } has weight  17.
    edge {  2,10 } has weight  13.
    edge {  2,11 } has weight  32.
    edge {  2,12 } has weight  11.
    edge {  2,13 } has weight  11.
    edge {  2,14 } has weight  11.
    edge {  2,15 } has weight  13.
    edge {  2,16 } has weight   7.
    edge {  3, 4 } has weight   5.
    edge {  3, 5 } has weight  16.
    edge {  3, 6 } has weight  13.
    edge {  3, 7 } has weight  12.
    edge {  3, 8 } has weight   6.
    edge {  3, 9 } has weight  16.
    edge {  3,10 } has weight  12.
    edge {  3,11 } has weight  31.
    edge {  3,12 } has weight  10.
    edge {  3,13 } has weight  10.
    edge {  3,14 } has weight  11.
    edge {  3,15 } has weight  12.
    edge {  3,16 } has weight   6.
    edge {  4, 5 } has weight  13.
    edge {  4, 6 } has weight  11.
    edge {  4, 7 } has weight  10.
    edge {  4, 8 } has weight   3.
    edge {  4, 9 } has weight  15.
    edge {  4,10 } has weight  11.
    edge {  4,11 } has weight  28.
    edge {  4,12 } has weight   8.
    edge {  4,13 } has weight   8.
    edge {  4,14 } has weight   8.
    edge {  4,15 } has weight   9.
    edge {  4,16 } has weight   5.
    edge {  5, 6 } has weight   4.
    edge {  5, 7 } has weight   6.
    edge {  5, 8 } has weight  11.
    edge {  5, 9 } has weight   8.
    edge {  5,10 } has weight   8.
    edge {  5,11 } has weight  16.
    edge {  5,12 } has weight   7.
    edge {  5,13 } has weight   7.
    edge {  5,14 } has weight   6.
    edge {  5,15 } has weight   4.
    edge {  5,16 } has weight  11.
    edge {  6, 7 } has weight   1.
    edge {  6, 8 } has weight   8.
    edge {  6, 9 } has weight   5.
    edge {  6,10 } has weight   4.
    edge {  6,11 } has weight  17.
    edge {  6,12 } has weight   3.
    edge {  6,13 } has weight   3.
    edge {  6,14 } has weight   3.
    edge {  6,15 } has weight   3.
    edge {  6,16 } has weight   8.
    edge {  7, 8 } has weight   7.
    edge {  7, 9 } has weight   5.
    edge {  7,10 } has weight   3.
    edge {  7,11 } has weight  18.
    edge {  7,12 } has weight   2.
    edge {  7,13 } has weight   2.
    edge {  7,14 } has weight   2.
    edge {  7,15 } has weight   3.
    edge {  7,16 } has weight   7.
    edge {  8, 9 } has weight  12.
    edge {  8,10 } has weight   8.
    edge {  8,11 } has weight  26.
    edge {  8,12 } has weight   5.
    edge {  8,13 } has weight   5.
    edge {  8,14 } has weight   5.
    edge {  8,15 } has weight   6.
    edge {  8,16 } has weight   2.
    edge {  9,10 } has weight   4.
    edge {  9,11 } has weight  15.
    edge {  9,12 } has weight   7.
    edge {  9,13 } has weight   7.
    edge {  9,14 } has weight   7.
    edge {  9,15 } has weight   8.
    edge {  9,16 } has weight  11.
    edge { 10,11 } has weight  19.
    edge { 10,12 } has weight   3.
    edge { 10,13 } has weight   4.
    edge { 10,14 } has weight   4.
    edge { 10,15 } has weight   6.
    edge { 10,16 } has weight   7.
    edge { 11,12 } has weight  20.
    edge { 11,13 } has weight  21.
    edge { 11,14 } has weight  20.
    edge { 11,15 } has weight  20.
    edge { 11,16 } has weight  25.
    edge { 12,13 } has weight   0.
    edge { 12,14 } has weight   1.
    edge { 12,15 } has weight   3.
    edge { 12,16 } has weight   5.
    edge { 13,14 } has weight   1.
    edge { 13,15 } has weight   3.
    edge { 13,16 } has weight   4.
    edge { 14,15 } has weight   2.
    edge { 14,16 } has weight   5.
    edge { 15,16 } has weight   7.

You have already seen how to draw a graph, here is the relevant code again.

```python
G = instance.get_graph()
if instance.is_depictable():
    pos = {i: instance.get_display(i) for i in instance.get_nodes()}
else:
    pos = nx.drawing.layout.spring_layout(G)
nx.draw_networkx_nodes(G, pos, node_color='#66a3ff', node_size=200)
#nx.draw_networkx_labels(G, pos, font_weight='bold' )
nx.draw_networkx_edges(G, pos,  edge_color='#e6e6e6')
```

# Implementing the MTZ model in Gurobi
We will implement the Miller-Tucker-Zemlin formulation using binary variables $x_{ij} \in \{0,1\}$ to indicate whether arc $(i,j)$ is being used in the tour and variables $y_i \ge 0$ as "position variables" for each node $i \in V$. The formulation looks like this:

\begin{align}
\min\;&\sum_{i,j \in V; i \ne j} c_{i,j} \cdot x_{i,j}\\
&\sum_{j \ne i} x_{i,j} = 1 \quad \text{for all nodes $i$}\\
&\sum_{j \ne i} x_{j,i} = 1 \quad \text{for all nodes $i$}\\
&y_1 = 1\\
&y_i \le n \quad \text{for all nodes $i \ne v_0$}\\
&y_i \ge 2 \quad \text{for all nodes $i \ne v_0$}\\
&y_i - y_j + (n-1) \cdot x_{i,j} \le n-2 \quad \text{for all nodes $i,j$ with $i,j \ne 1$ and $i \ne j$}\\
&x_{i,j} \in \{0,1\}\\
&y_i \ge 0
\end{align}

## Creating the variables
We start by creating the model and the variables. Notice that we already define the objective function by using the `obj` parameter upon variable creation. For demonstration purposes, we use a loop for defining the $x$ variables and a shortcut function for the $y$ variables. Note that the assumption $y_i \ge 0$ is implicit.

```python
import gurobipy as grb

# create a new model
model = grb.Model(name="MTZ TSP formulation")

# create dictionary with tupel as keys
# elements of x can then be accesses via x[i,j]
x = grb.tupledict()
# filter discards all elements that do not fit the filter criteria
for i,j in filter(lambda e: e[0] != e[1], instance.get_edges()):
    # add variable with objective coefficient obj, type {0,1} (binary)
    x[i,j] = model.addVar(obj=instance.wfunc(i,j), vtype=grb.GRB.BINARY, name=f"x[{i},{j}]")

# add variable for every element of instance.get_nodes()
y = model.addVars(instance.get_nodes(), name="y")
```

## Adding the constraints
Next, we add the constraints for our model. Notice that $y_i \ge 0$ is implicit and $x_{i,j} \in \{0,1\}$ has been included in the variable definition already. We start with the simple constraints on the $y_i$ variables. Notice that we could also have used methods to set lower and upper bounds for the $y_i$ for most of the simple constraints.

```python
model.addConstr(y[first_node] == 1)
for i in itertools.islice(instance.get_nodes(),first_node+1,None):
    model.addConstr(y[i] <= instance.dimension)
```

**Task 1:** Add another loop to formulate the constraints $y_i \ge 2$ for all nodes $i \ne v_0$.

```python

```

The remaining constraints require iteration over all edges. We use the keys() set of our variables for this purpose to ensure we only iterate over edges that are actually defined.

```python
for i,j in filter(lambda e: e[0] != e[1] and e[0] != first_node and e[1] != first_node, x.keys()):
    model.addConstr(y[i] - y[j] + (instance.dimension-1)*x[i,j] <= (instance.dimension-2))
```

**Task 2:** Add the in- and out-degree constraints
\begin{align}
&\sum_{j \ne i} x_{i,j} = 1 \quad \text{for all nodes $i$}\\
&\sum_{j \ne i} x_{j,i} = 1 \quad \text{for all nodes $i$}\\
\end{align}

```python

```

## Starting the Optimization Process
Finally, we set the objective to minimization and call the optimizer.

```python
model.ModelSense = grb.GRB.MINIMIZE
model.optimize()
```

    Optimize a model with 240 rows, 256 columns and 660 nonzeros
    Variable types: 16 continuous, 240 integer (240 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 2e+01]
      Objective range  [1e+00, 3e+01]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+00, 2e+01]
    Found heuristic solution: objective 0.0000000

    Explored 0 nodes (0 simplex iterations) in 0.01 seconds
    Thread count was 1 (of 8 available processors)

    Solution count 1: 0

    Optimal solution found (tolerance 1.00e-04)
    Best objective 0.000000000000e+00, best bound 0.000000000000e+00, gap 0.0000%

## Querying and Visualizing the Solution
Before we visualize our result, let us look at a few key figures of our solution.

```python
print(f"Objective value: {model.ObjVal}")
print(f"value of x variables: {model.x}")

solution_edges = [(i,j) for i,j in x.keys() if x[i,j].x > 0.9]
solution_edges

```

    Objective value: 0.0
    value of x variables: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

    []

For debugging purposes, it might be helpful to export the model held by Gurobi into a human-readable format:

```python
model.write('test.lp')
```

 Finally, let us visualize the solution using NetworkX. In this case, we need to prescribe positions and draw the nodes and two layers of edges separately.

```python
if instance.is_depictable():
    pos = {i: instance.get_display(i) for i in instance.get_nodes()}
else:
    pos = nx.drawing.layout.spring_layout(G)
nx.draw_networkx_nodes(G, pos, node_color='#66a3ff', node_size=500)
nx.draw_networkx_labels(G, pos, font_weight='bold' )
nx.draw_networkx_edges(G, pos,  edge_color='#e6e6e6')
nx.draw_networkx_edges(G, pos,  edgelist=solution_edges, edge_color='#ffa31a', width=4)
```

```python

```


# System Properties

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Causality

The [principle of causality](https://en.wikipedia.org/wiki/Causality) states that an effect has to follow its cause temporally. Theoretical systems may violate this fundamental principle which prohibits their practical applicability. Causality is an important aspect for the practical realization of systems. Conditions for the impulse response of a linear time-invariant (LTI) causal system as well as for its transfer function are derived in this section.

### Condition for the Impulse Response

The output signal $y(t) = \mathcal{H} \{ x(t) \}$ of an LTI system is given by convolving the input signal $x(t)$ with its impulse response $h(t)$

\begin{equation}
y(t) = \int_{-\infty}^{\infty} x(\tau) \cdot h(t-\tau) \; d\tau = \int_{-\infty}^{\infty} x(t - \tau) \cdot h(\tau) \; d\tau
\end{equation}

Analyzing the first integral reveals that the computation of the output signal $y(t)$ for some time instant $t=t_0$ requires knowledge of the input signal for all time instants. This includes also future time instants $t > t_0$. While this does not pose a problem for signals and impulse responses given in closed-form, this is not feasible in practice. Without imposing further restrictions, this would require the knowledge of the input signal for all future time instances.

Causality is not violated if we modify the integration limits of the convolution integral

\begin{equation}
y(t) = \int_{-\infty}^{t} x(\tau) \cdot h(t-\tau) \; d\tau = \int_{0}^{\infty} x(t - \tau) \cdot h(\tau) \; d\tau
\end{equation}

Now the output signal $y(t)$ for a given time instant $t=t_0$ depends only on the input signal $x(t)$ for $t \leq t_0$. Comparing the second equality with the second equality of the original convolution integral yields that the modified integration limits are equal to the assumption that  $h(t) = 0$ for $t < 0$.

A system is termed *causal system* [iff](https://en.wikipedia.org/wiki/If_and_only_if) its impulse response $h(t)$ is a causal signal

\begin{equation}
h(t) = 0 \qquad \text{for } t < 0
\end{equation}

Only causal systems can be practically realized due to above reasoning.

### Condition for the Transfer Function

The transfer function $H(j \omega) = \mathcal{F} \{ h(t) \}$ of a causal system shows specific symmetries. A causal impulse response $h(t)$ has to fulfill the following relation

\begin{equation}
h(t) = h(t) \cdot \epsilon(t)
\end{equation}

Fourier transformation of the left- and right-hand side yields

\begin{equation}
H(j \omega) = \frac{1}{\pi} H(j \omega) * \frac{1}{j \omega}
\end{equation}

by application of the [multiplication theorem](../fourier_transform/theorems.ipynb#Multiplication-Theorem) in conjunction with the [Fourier transform of the Heaviside signal](../fourier_transform/theorems.ipynb#Transformation-of-the-Heaviside-signal). Decomposing this result into the real and imaginary part of $H(j \omega)$ derives the following symmetry relations for a causal system

\begin{align}
\Re \{ H(j \omega) \} &= \frac{1}{\pi} \Im \{ H(j \omega) \} * \frac{1}{\omega} \\
\Im \{ H(j \omega) \} &= - \frac{1}{\pi} \Re \{ H(j \omega) \} * \frac{1}{\omega}
\end{align}

The convolution of a spectrum with $\frac{1}{\omega}$ is known as [Hilbert transform](https://en.wikipedia.org/wiki/Hilbert_transform). Above result states that the real and imaginary part of the spectrum $H(j \omega)$ of a causal system are related by the Hilbert transform.

## Stability

The stability of a system can be evaluated with respect to different criteria. The most common ones are [Lyapunov stability](https://en.wikipedia.org/wiki/Lyapunov_stability) and [bounded-input bounded-output stability](https://en.wikipedia.org/wiki/BIBO_stability). The latter is introduced in the following.

### Bounded-Input Bounded-Output Stability

With respect to the bounded-input bounded-output (BIBO) principle, an LTI system is termed stable if its output signal $y(t) = \mathcal{H} \{ x(t) \}$ is bounded for a bounded input signal $x(t)$. A signal is bounded if its magnitude does not exceed a given finite value. For the in- and output this condition is formulated as

\begin{align}
|x(t)| &< M_1 \\
|y(t)| &< M_3
\end{align}

where $M_1, M_3 < \infty$ denote constant finite bounds. The BIBO criterion is illustrated in the following

#### Condition for the Impulse Response

In order to derive a condition for the impulse response $h(t)$ of an LTI system that conforms to the BIBO criterion, the magnitude of the output signal $y(t)$ is expressed by convolving the input signal $x(t)$ with the impulse response

\begin{equation}
|y(t)| = \left| \int_{-\infty}^{\infty} x(\tau) \cdot h(t-\tau) \; d\tau \right|
\end{equation}

An upper bound for the magnitude of the output signal is found by applying the [triangle inequality](https://en.wikipedia.org/wiki/Triangle_inequality) together with the upper bound for $|x(t)|$

\begin{equation}
|y(t)| \leq \int_{-\infty}^{\infty} |x(\tau)| \cdot |h(t-\tau)| \; d\tau < \int_{-\infty}^{\infty} M_1 \cdot |h(t-\tau)| \; d\tau
\end{equation}

Since the output signal $|y(t)|$ shall be bounded, it can be concluded that the impulse response needs to be an [absolutely integrable function](https://en.wikipedia.org/wiki/Absolutely_integrable_function)

\begin{equation}
\int_{-\infty}^{\infty} |h(t)| \; dt < M_2
\end{equation}

where $M_2 < \infty$ denotes a constant finite bound.

An LTI system is stable in the sense of the BIBO stability criterion iff its impulse response is absolutely integrable. Since absolute integrability of a signal is sufficient for the [existence of its Fourier transform](../fourier_transform/definition.ipynb#Definition), this implies that the transfer function $H(j \omega) = \mathcal{F} \{ h(t) \}$ exists.

#### Conditions for Rational Transfer Functions

For a rational transfer function $H(s)$, the BIBO stability of an LTI system implies constraints on the locations of its poles. These constraints are derived in the following.

The inverse Laplace transform of a right-sided signal can be computed in closed form [using the partial fraction decomposition](../laplace_transform/inverse.ipynb#Basic-Procedure) of its Laplace transform. Applying this to the transfer function $H(s)$, the impulse response $h(t) = \mathcal{L}^{-1} \{ H(s) \}$ is given as

\begin{equation}
h(t) = A_0 \cdot \delta(t) + \epsilon(t) \sum_{\mu = 1}^{L} e^{s_{\infty \mu} t} \sum_{\nu = 1}^{R_\mu} \frac{A_{\mu \nu} \, t^{\mu - 1}}{(\nu -1)!}
\end{equation}

where $s_{\infty \mu}$ denotes the $\mu$-th unique pole of $H(s)$, $R_\mu$ its degree, $L$ the total number of different poles $\mu = 1 \dots L$, and $A_0$ and $A_{\mu \nu}$ the coefficients of the partial fraction decomposition. Above formula for the inverse Laplace transform holds only if the order $M$ of the numerator is smaller or equal to the order $N$ of the denominator. If $M > N$, the impulse response is not absolutely integrable. Consequently the system is not stable in the BIBO sense.

For $M \leq N$ one has to check under which conditions above impulse response $h(t)$ is absolutely integrable. The first term $A_0 \delta(t)$ does not pose a problem for $A_0 < \infty$. Under the assumption $A_{\mu \nu} < \infty$, the absolute integrability of the second term is checked by evaluating

\begin{equation}
\int_{-\infty}^{\infty} |\epsilon(t) t^{\mu - 1} e^{s_{\infty \mu} t}| \; dt = \int_0^{\infty} t^{\mu - 1} e^{\sigma_{\infty \mu} t} \; dt
\end{equation}

where $s_{\infty \mu} = \sigma_{\infty \mu} + j \omega_{\infty \mu}$. Above integral is absolutely integrable if $\sigma_{\infty \mu} < 0$.

From above results it can be concluded that a system with rational transfer function $H(s)$ and right-sided impulse response $h(t)$ is stable with respect to the BIBO criterion if

* the order $M$ of its numerator is smaller or equal to the order $N$ of its numerator and
* all poles are located in the left $s$-half-plane
    \begin{equation}
    \Re \{ s_{\infty \mu} \} < 0 \qquad \forall \mu
    \end{equation}

The locations of the zeros of the transfer function have no influence on the stability of a system.

In order to investigate the stability of a given system the poles of the rational transfer function $H(s)$ have to be determined. This can be performed by computing the roots of the denominator polynomial. However, in order to avoid the computationally complex computation of roots, several stability tests have been developed. These determine the stability of a system from the coefficients of the denominator polynomial. An example of such a test is the [Routh–Hurwitz stability criterion](https://en.wikipedia.org/wiki/Routh%E2%80%93Hurwitz_stability_criterion).

**Example**

The impulse response $h(t) = \mathcal{L}^{-1} \{ H(s) \}$ of a 2nd-order system with transfer function

\begin{equation}
H(s) = \frac{1}{(s-s_\infty)(s-s^*_\infty)}
\end{equation}

is computed. First the case of a stable system is considered by assuming $\Re \{ s_\infty \} < 0$ with $s_\infty = -1 + j$. The impulse response $h(t)$ is computed by inverse Laplace transform of the transfer function

```python
%matplotlib inline
import sympy as sym
sym.init_printing()

s = sym.symbols('s', complex=True)
t = sym.symbols('t', real=True)
s_inf = -1 + sym.I

H = 1/((s - s_inf)*(s - sym.conjugate(s_inf)))
h = sym.inverse_laplace_transform(H, s, t)
h
```

The impulse response is plotted for illustration

```python
sym.plot(h, (t, 0, 10), xlabel='$t$', ylabel='$h(t)$');
```

It can be observed that the amplitude of the impulse response decays exponentially with increasing time $t$. The absolute integrability of the impulse response is confirmed by noting that

\begin{equation}
\int_{-\infty}^{\infty} | \epsilon(t) e^{-t} \sin(t) | \; dt = \int_{0}^{\infty} | e^{-t} \sin(t) | \; dt < \int_{0}^{\infty} | e^{-t} | \; dt = 1
\end{equation}

Now the impulse response of an instable system is computed by assuming $\Re \{ s_\infty \} > 0$ with $s_\infty = 1 + j$

```python
s_inf = 1 + sym.I

H = 1/((s - s_inf)*(s - sym.conjugate(s_inf)))
h = sym.inverse_laplace_transform(H, s, t)
h
```

Again the impulse response is plotted for illustration

```python
sym.plot(h, (t, 0, 10), xlabel='$t$', ylabel='$h(t)$');
```

The impulse response shows exponentially increasing negative values for increasing time. The impulse response is clearly not absolutely integrable and hence the system not stable in the BIBO sense.

## Realizability

A system is said the be realizable if it is a causal and stable system. Realizability is a prerequisite for the practical implementation of systems. Causality implies that the impulse response $h(t)$ of the system has to be a causal signal. Hence the region of convergence (ROC) of its transfer function $H(s) = \mathcal{L} \{ h(t) \}$ is the half-plane located right of all poles. Stability and causality of a system with rational transfer function $H(s)$ implies that its poles are located in the left $s$-half-plane.

Summarizing, a system with rational transfer function $H(s)$ is realizable iff all poles of $H(s)$ are located in the left $s$-half-plane not including the imaginary axis and the order of the numeration is lower or equal to the order of the denominator. The ROC is given by the right $s$-half-plane including the imaginary axis. The locations of the zeros of the transfer function have no influence on the realizability of systems.

**Copyright**

The notebooks are provided as [Open Educational Resource](https://de.wikipedia.org/wiki/Open_Educational_Resources). Feel free to use the notebooks for your own educational purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Lecture Notes on Signals and Systems* by Sascha Spors.


Validation of the NN code generator for the 2D diffusion equation

O. Pannekoucke

$   %\newcommand{\pde}{\partial}
    %\newcommand{\pdt}{\partial_t}
    %\newcommand{\pdx}{\partial_x}
    \newcommand{\bu}{\bar u}
    \newcommand{\eps}{\varepsilon}$

 Objectifs

 * Definition of the 2D diffusion equation by using `sympy`
 * Computation of the numerical solution of the 2D diffusion using a NN

---

Contents

  1. [Introduction](#intro)
  1. [Dynamics](#model)
  1. [Numerical code for the resolution](#code)
  1. [Numerical application](#num)
  1. [Conclusion](#conclusion)
---

## Introduction

The aim is to compute the solution of the diffusion equation given by
$$\pdt u = \partial_{x^i}\left(\kappa_{ij}\partial_{x^j} u \right),$$
where $\kappa$  is a field of diffusion tensors.

## Dynamics

```python
from sympy import Function, symbols, init_printing, Derivative, Matrix
from pdenetgen import Eq, CoordinateSystem
init_printing()
```

**Set the diffusion equation**

```python
t, x, y = symbols('t x y')
u = Function('u')(t,x,y)
kappa11 = Function('\\kappa_{11}')(x,y)
kappa12 = Function('\\kappa_{12}')(x,y)
kappa22 = Function('\\kappa_{22}')(x,y)

diffusion_in_2D = Eq(Derivative(u,t),
  Derivative(kappa11*Derivative(u,x)+kappa12*Derivative(u,y) ,x)+
  Derivative(kappa12*Derivative(u,x)+kappa22*Derivative(u,y) ,y)).doit()
```

## Numerical code for the resolution

### Symbolic to numerical code

```python
from pdenetgen import NNModelBuilder
```

```python
cas_model = NNModelBuilder(diffusion_in_2D, class_name="NNDiffusion2DHeterogenous")
```

```python
cas_model.constant_functions
```

```python
print(cas_model.code)
```

```python
infile = False
if infile:
    # -1- Write module
    cas_model.write_module()
    # -2- Load module
    exec(f"from {cas_model.module_name} import {cas_model.class_name}")
else:
    exec(cas_model.code)
```

## Numerical application

### Definition of the domain of computation

The domain is the bi-periodic sqare $[0,1)\times [0,1)$

### Set the numerical NN model

```python
num_model = NNDiffusion2DHeterogenous()
domain = num_model
```

**Set initial fields**

```python
import numpy as np
```

```python
dx, dy = num_model.dx

# Set a dirac at the center of the domain.
U = np.zeros(num_model.shape)
U[num_model.shape[0]//2, num_model.shape[0]//2] = 1./(dx*dy)

```

```python
num_model.shape
```

```python
X = np.asarray(num_model.X)
k = np.asarray([1,2])
X = np.moveaxis(X,0,2)
print(X.shape)

np.linalg.norm(X@k -k[0]*num_model.X[0]-k[1]*num_model.X[1])
```

**Set constants and time step**

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

```python
time_scale = 1.
#
# Construction du tenseur de diffusion
#

# a) Définition des composantes principales
lx, ly = 10*dx, 5*dy
kappa_11 = lx**2/time_scale
kappa_22 = ly**2/time_scale

# b) Construction d'un matrice de rotation
R = lambda theta : np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])

# c) Spectre du tenseur de référence
D = np.diag([kappa_11,kappa_22])

# d) Set diffusion tensors field
num_model.kappa_11 = np.zeros(num_model.shape)
num_model.kappa_12 = np.zeros(num_model.shape)
num_model.kappa_22 = np.zeros(num_model.shape)

X = np.moveaxis(np.asarray(num_model.X),0,2)
k = 2*np.pi*np.array([2,3])

theta = np.pi/3*np.cos(X@k)
#plt.contourf(*num_model.x, theta)

for i in range(num_model.shape[0]):
    for j in range(num_model.shape[1]):
        lR = R(theta[i,j])
        nu = lR@np.diag([kappa_11,kappa_22])@lR.T
        num_model.kappa_11[i,j] = nu[0,0]
        num_model.kappa_12[i,j] = nu[0,1]
        num_model.kappa_22[i,j] = nu[1,1]

num_model.kappa_11 = num_model.kappa_11.reshape((1,100,100,1))
num_model.kappa_12 = num_model.kappa_12.reshape((1,100,100,1))
num_model.kappa_22 = num_model.kappa_22.reshape((1,100,100,1))
#
# Calcul du pas de temps adapté au problème
#
dt = np.min([dx**2/kappa_11, dy**2/kappa_22])

CFL = 1/6
num_model._dt = CFL * dt
print('time step:', num_model._dt)
```

**Illustrates trend at initial condition**

```python
def plot(field):
    plt.contourf(*num_model.x, field.T)
```

```python
state0 = U.copy().reshape((1,1)+num_model.shape+(1,))
print(state0.shape)
```

```python
import keras
```

```python
dU, = num_model.trend(0,state0)

plot(dU[0].reshape((100,100)))
plt.title('Trend for the diffusion')
```

**Short forecast**

```python
times = num_model.window(time_scale)
#saved_times = times[::100]
saved_times = times
```

```python
num_model.set_time_scheme('euler')
traj = num_model.forecast(times, state0, saved_times)
```

```python
plt.figure(figsize=(12,5))

start, end = [traj[time] for time in [saved_times[0], saved_times[-1]]]

title = ['start', 'end']
for k, state in enumerate([start, end]):
    plt.subplot(121+k)
    plot(state[0].reshape((100,100)))
    plt.title(title[k])

plt.savefig('./figures/NN-diffusion-2D-prediction.pdf')
np.save('nn-diffusion.npy', end)
```

**Comparison with the finite difference solution**

```python
fd_end_solution = np.load('fd-diffusion.npy')
```

```python
np.linalg.norm(fd_end_solution[0] - end[0,0,:,:,0])
```

## Conclusion

In this notebook, the solution of the diffusion equation using a NN code has been presented.
The results are those of the finite-difference solution (not shown here). This validate the NN generator and illustrates how the physical equations can be used for the design of a NN architecture.


```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = (9,9)
```

HTML [Demo app](demo.html)

# Lattice invariants and quadratic forms

Let $x\rightarrow Mx$ transform is a lattice invariant (i.e. $M$ is integer, and $\det(M)=1$).

Then how to find equation $x'Ax = 1$ that is unchagned by it? Matrix $A$ must satisfy:

$$ M'AM=A $$

This is Lyapunov equation

Using Maxima, the following solution was obtained:
$$ A = \left(\begin{matrix}
  2 c & d-a\\
  d-a & -2 b
\end{matrix}\right) $$

where

$$ M=\left(\begin{matrix}a & b \\ c & d\end{matrix}\right) $$.

```python
def tfm2qform(m):
    """Convert identity transofrm to quadratic form matrix, simpler implementation"""
    (a,b),(c,d) = m
    return np.array([[2*c,d-a],[d-a, -2*b]])
```

```python
tfm2qform([[2,1],[1,1]])
```

    array([[ 2, -1],
           [-1, -2]])

Maxima code:
```m:matrix([a,b],[c,d]);
aa:matrix([x,y],[y,z]);
mam : ratsimp(transpose(m).aa.m);
zz: mam - aa*determinant(m);
ss:solve([zz[1,1],zz[1,2],zz[2,2]],[x,y,z]);
subst(%r2=1,subst(ss[1], aa)*2*c);
```

```python
def lattice(v,n=5, show=True):
    x = np.linspace(-n,n,n*2+1)
    xx,yy = np.meshgrid(x,x)
    ppp = np.stack((xx.ravel(),yy.ravel()))
    vvv = np.dot(v, ppp)
    plt.plot(vvv[0,:],vvv[1,:],"bo")
    show and plt.show()

```

```python
lattice([[2,1],[1,1]])
```

# Double neighbor
The most important question for the CA simulator: given 2 cells, find their common neighbors.

Let the translation vector between cells is $p$. Then the answer is given by integer solution of the intersection of 2 equal conics, one is central, second is translated by $p$:
$$\begin{cases} x^TAx = c \\
                (x-p)^TA(x-p) = c
  \end{cases}$$

Expand 2nd

$$ x^TAx -2p^TAx+p^TAp = c  $$
$$ 2p^TAx=p^TAp $$

therefore
$$x = \frac12 p + tq$$

where $p^TAq=0$. The latter means that $q \perp Ap$, and also $Aq \perp p$

substituting into 1st:

$$ (\frac12 p+tq)'A(1/2p+tq) = c$$
$$ \frac14 p'Ap + t p'Aq + t^2 q'Aq = c$$

Since $p'Aq=0$, this gives
$$ \frac14 p'Ap + t^2 q'Aq = c$$

thus,
$$t = \sqrt{ \frac{c-\frac14p'Ap}{q'Aq} }$$

therefore
$$ x =  \frac12 p \pm q\sqrt{ \frac{c-\frac14p'Ap}{q'Aq} }$$

In 2d case, $q = RAp$ or $A^{-1}Rp$, where
$$R=\left( \begin{matrix} 0 & -1 \\ 1 & 0  \end{matrix} \right)$$

THus $q'Aq = p'AR'ARAp$ or $q'Aq = p'R'A^{-1}Rp$

Let's check whether $RA$ is reallly proportional to $A^{-1}R$...

```python
a=np.array([[2,1],[1,3]])
r=np.array([[0,-1],[1,0]])

print(r.dot(a))
print(np.linalg.inv(a).dot(r) * np.linalg.det(a))
```

    [[-1 -3]
     [ 2  1]]
    [[-1. -3.]
     [ 2.  1.]]

Yes, this seems to be right. Numeric check confirms. that

$$RA = \det(A) A^{-1}R = A^+R$$

From now on, use $q=RAp = \det(A)A^{-1}Rp$

Thus, $ p'AR'ARAp = \det(A) p'AR'A A^{-1}Rp = \det(A) p'Ap$

Therefore,
$$q'Aq = \det(A)p'Ap$$

Thus,
$$ x =  \frac12 p \pm \frac12 q\sqrt{ \frac{4c-p'Ap}{\det(A)p'Ap} }$$
or, substituting $q$, finally obtaining

$$ x =  \frac12 p \pm \frac12 RAp\sqrt{ \frac{4c-p'Ap}{\det(A)p'Ap} }$$

```python
#test code
import math
def intersection(a, p, c):
    p=np.array(p)
    pap = p.dot(a).dot(p)
    if pap == 0:
        return None
    da = np.linalg.det(a)
    ra=np.dot([[0,-1],[1,0]], a)
    print(f"pap={pap}, |a|={da}")
    D = (4*c-pap)/(da*pap)
    if D >= 0:
        Q = math.sqrt(D)
        x1 = 0.5*p + 0.5*Q*ra.dot(p)
        x2 = 0.5*p - 0.5*Q*ra.dot(p)
        return x1,x2

```

```python
intersection([[2,-1],[-1,-2]], [3,1], 2)
```

    pap=10, |a|=-5.000000000000001

    (array([2., 1.]), array([1.00000000e+00, 5.55111512e-17]))

```python
from math import gcd
from libeuler import numtheor as nt

def int_intersection(a, p, c):
    p=np.array(p, dtype=np.int64)
    pap = int(p.dot(a).dot(p))
    if pap == 0:
        return None
    da = int(np.linalg.det(a))
    ra=np.dot([[0,-1],[1,0]], a).astype(np.int64)
    print(f"pap={pap}, |a|={da}")
    Dnum = 4*c-pap
    Dden = da*pap
    print("num, den:",(Dnum, Dden))
    g = gcd(Dnum, Dden)
    Dnum /= g
    Dden /= g
    if Dden < 0:
        Dnum = -Dnum
        Dden = -Dden

    if Dnum >= 0:
        qnum = nt.sqrti(Dnum)
        if qnum*qnum != Dnum:
            #not a full square
            return None
        qden = nt.sqrti(Dden)
        if qden*qden != Dden:
            #not a fu;ll square again
            return None

        rapx, rapy = (qnum*ra.dot(p)).astype(np.int64)
        rapx1, m = divmod(rapx, qden)
        if m != 0: return None
        rapy1, m = divmod(rapy, qden)
        if m != 0: return None

        twox1 = p + [rapx1, rapy1]
        twox2 = p - [rapx1, rapy1]
        if twox1[0] %2 != 0 or twox1[1]%2 != 0:
            return None
        return twox1 // 2, twox2 // 2
```

```python
m = np.array([[2,1],[1,1]], dtype=np.int64)
a = tfm2qform(m).astype(np.int64)

c = np.dot([1,0],a).dot([1,0]).astype(np.int64)

#make neighbor

n1 = np.linalg.matrix_power(m,4).dot([1,0]).astype(np.int64)
n2 = np.linalg.matrix_power(m,-3).dot([1,0]).astype(np.int64)

print("Neighbors:", n1, n2)
print(intersection(a,n1+n2,c))
print(int_intersection(a,n1+n2,c))

```

    Neighbors: [34 21] [ 5 -8]
    (array([ 34.,  21.]), array([ 5., -8.]))
    pap=1690, |a|=-5
    num, den: (-1682, -8450)
    (array([ 34.,  21.]), array([ 5., -8.]))

```python
print(int_intersection(a,np.array([-100, -75]),c))
```

    pap=-6250, |a|=-5
    num, den: (6258, 31250)
    None

```python
#Plotting order 2 neighbors
def plot_order_2_neighbors(n, a, c):
    ptx = []
    pty = []
    for x in range(-n,n+1):
        for y in range(-n,n+1):
            if int_intersection(a,np.array([x,y], dtype=np.int64),c) is not None:
                ptx.append(x)
                pty.append(y)
    plt.plot(ptx,pty, "ro")

```

```python
plot_order_2_neighbors(30, a, c)
plt.show()
```

```python
#approximate neighbor order
def plot_approx_neighbor_order(n, nbig, m, x0, max_points, max_order):
    a = tfm2qform(m)

    #prepare neighbor offsets
    invm = np.linalg.inv(m).astype(np.int64)
    def make_offsets(mtx):
        ofs = []
        x = x0
        while True:
            x = mtx.dot(x)
            if np.max(np.abs(x))<nbig*2:
                ofs.append(x)
            else:
                break
        return ofs
    offsets = make_offsets(m)[::-1]+[x0]+make_offsets(invm)
    print(f"Prepared {len(offsets)} neighbor offsets in range +-{2*nbig}")

    orig = (0,0)
    orders = np.zeros((2*n+1,2*n+1),dtype=np.int16)
    orders -= 1

    norders = [orders.shape[0]*orders.shape[1]]

    def putpixel(x,y,v):
        orders[x+n, y+n]=v
        norders[0] -= 1

    putpixel(0,0,0)

    visited = set((orig,))
    order2neighbors = {0: [orig]}

    def try_neighbor(order, x,y):
        if abs(x)>nbig or abs(y)>nbig:
            return
        k = (x,y)
        if k in visited:
            return
        visited.add(k)
        if -n <= x <= n and -n <=y <= n:
            putpixel(x,y,order)
        order2neighbors[order].append(k)
    order = 0 #current order
    while True:
        order1 = order + 1
        order2neighbors[order1] = []
        for nx, ny in order2neighbors[order]:
            for dx, dy in offsets:
                try_neighbor(order1, nx+dx,ny+dy)
                try_neighbor(order1, nx-dx,ny-dy)
        order = order1
        print(f"COmpleted order {order}, has {len(order2neighbors[order-1])} points")
        total = sum(len(ns) for ns in order2neighbors.values())
        if total > max_points:
            print("Maximal number of points reached")
            break
        if order > max_order:
            print(f"Reached max order {max_order}")
            break
        if norders[0] == 0:
            print(f"Filled all points in the desired area, exiting")
            break

    del order2neighbors
    del visited
    xx = np.array(range(-n,n+1))
    plt.matshow(orders, extent=[-n,n,-n,n], cmap=plt.cm.Spectral)

```

```python
#def plot_approx_neighbor_order(n, nbig, m, x0, max_points, max_order):

plot_approx_neighbor_order(20, 3000, m=m, x0=[1,0], max_points=4000000, max_order=11)
plt.colorbar()
plt.show()
```

```python

```

### How 2 hyperbolas can be non-intersecting

```python
def plot_no_intersection(a, c, xmax=40):
    xs = []
    ys = []
    for x in np.linspace(-xmax, xmax+1,100):
        for y in np.linspace(-xmax, xmax+1, 100):
            if intersection(a, [x,y], c) is None:
                xs.append(x)
                ys.append(y)
    plt.plot(xs,ys,"r.")
```

### Points that can not be decomposed as sum of 2 order-1 neighbors

```python
plot_no_intersection(a, c, 10)
plt.show()
```

## Intersection os non-equal hyperbolas
What is 2 hyperbolas have different constant term?

$$\begin{cases} x'Ax = c_1 \\
                (x-p)'A(x-p) = c_2
  \end{cases}$$

Expand 2nd

$$ x'Ax - 2p'Ax + p'Ap = c_2  $$
$$ 2p'Ax = c_2-c_1 + p'Ap $$
$$ p'Ax = \frac{\Delta c}2 + \frac12 p'Ap$$

Decompose $s$ as sum of 2 vectors: $p, q$, where $q \perp Ap$

$$x = w p + t q$$

and substitute:

$$w p'Ap = \frac{\Delta c}2 + \frac12 p'Ap$$

## Intersection os non-equal hyperbolas
What is 2 hyperbolas have different constant term?

$$\begin{cases} x'Ax = c_1 \\
                (x-p)'A(x-p) = c_2
  \end{cases}$$

Expand 2nd

$$ x'Ax - 2p'Ax + p'Ap = c_2  $$
$$ 2p'Ax = c_1-c_2 + p'Ap $$
$$ p'Ax = \frac{\Delta c}2 + \frac12 p'Ap$$

Decompose $s$ as sum of 2 vectors: $p, q$, where $q \perp Ap$, $q=ARp$

In this case, $q'Aq = \det(A)p'Ap$

$$x = w p + t q$$

and substitute:

$$w p'Ap = \frac{\Delta c}2 + \frac12 p'Ap$$
$$w = \frac{\Delta c + p'Ap}{2 p'Ap} = \frac12 + \frac{\Delta c}{2 p'Ap}  $$

and $t$ is any.

```python
import sympy as sp
import sympy.abc as abc
sp.init_printing(use_latex="mathjax")
w, t = abc.w, abc.t
pap = sp.Symbol('pap')
c1, c2 = map(sp.Symbol, ('c1','c2'))
dc = c1-c2
w0 = sp.solve(w*pap -(dc/2 + pap/2), w)[0]
w0
```

$$\frac{1}{2 pap} \left(c_{1} - c_{2} + pap\right)$$

```python
da = sp.Symbol("da")
sols_t = sp.solve(w0**2*pap + t**2*da*pap - c1, t)
sols_t
```

$$\left [ \frac{1}{2 pap} \sqrt{\frac{1}{da} \left(- c_{1}^{2} + 2 c_{1} c_{2} + 2 c_{1} pap - c_{2}^{2} + 2 c_{2} pap - pap^{2}\right)}, \quad - \frac{1}{2 pap} \sqrt{- \frac{1}{da} \left(c_{1}^{2} - 2 c_{1} c_{2} - 2 c_{1} pap + c_{2}^{2} - 2 c_{2} pap + pap^{2}\right)}\right ]$$

```python
t0 = sols_t[0]
p, q = map(sp.Symbol, ('p', 'q'))
w0*p + t0*q

```

$$\frac{p}{2 pap} \left(c_{1} - c_{2} + pap\right) + \frac{q}{2 pap} \sqrt{\frac{1}{da} \left(- c_{1}^{2} + 2 c_{1} c_{2} + 2 c_{1} pap - c_{2}^{2} + 2 c_{2} pap - pap^{2}\right)}$$

Ok, this is way more clean expresstion
$$\frac{
    p(c_1-c_2 + pap) + q\sqrt{
        \frac1{da}\left(-(c_1-c_2)^2+2(c_1+c_2)pap - pap^2\right)
    }
  }{
    2 pap
  }$$

$$\frac{
    p(c_1-c_2 + pap) + q\sqrt{
        -\frac1{da}\left((pap-c_1-c_2)^2-4c_1c_2\right)
    }
  }{
    2 pap
  }$$

```python
#Determinant of A is:
np.linalg.det(a)
```

$$-5.0$$

Thus, it is negative, and should always be. Therefore, expression under the square root is positive for most values.

FOr large $p'Ap$, root, when it exists, is equal to $ceil \left|\frac{pap-c_1-c_2}{\sqrt{\det(A)}}\right|$

## Show cell neighbors

```python
m = np.array([[2,1],[1,1]])
def plot_neighbors(n0,xmax=10):
    im = np.linalg.inv(m)
    xy = n0
    def powers(m):
        xy = n0
        points = []
        while np.max(np.abs(xy)) < xmax:
            xy = m.dot(xy)
            points.append(xy)
        return points
    points = np.stack(powers(im) + [n0] + powers(m))
    plt.plot(points[:,0], points[:,1], "rx")

plot_neighbors([1,0])
plot_neighbors([-1,0])
plt.plot([0],[0],'bo')
plt.show()

```

## Find cell with integer coordinates and minimal distance to the origin

```python
def mindist(a):
    """find integer of the minimal pseudo-distance, minimizing
    x'ax
    """
    dmin = None
    pmin = []
    for x in range(-10,10):
        for y in range(-10,10):
            if not (x or y): continue
            p = np.array([x,y])
            d = int(abs(p.dot(a).dot(p)))
            dd = (d, abs(x)+abs(y)) #search for points that have simplest coordinates
            if (dmin is None) or (dd < dmin):
                dmin = dd
                pmin = [p]
            elif dd == dmin:
                pmin.append(p)
    return pmin, dmin[0]

a = tfm2qform([[2,1],[1,1]])
points, dmin = mindist(a)
print("A matrix is")
print(a)
print(f"Minimal pseudo-distance is {dmin}, achieved for points:")
for p in points:
    print(f"  {p}")
```

    A matrix is
    [[ 2 -1]
     [-1 -2]]
    Minimal pseudo-distance is 2, achieved for points:
      [-1  0]
      [ 0 -1]
      [0 1]
      [1 0]

# Rewrite systems for minkovsky grids

Consider group with 2 generators:

* $r$ : hyperbolic rotation
* $t$ : grid translation by 1

Matrix representations:

```python
r = np.array([[2,1,0],[1,1,0],[0,0,1]])
t = np.array([[1,0,1],[0,1,0],[0,0,1]])
R = np.linalg.inv(r).astype(np.int)
T = np.linalg.inv(t).astype(np.int)

def dots(*mts):
    m = mts[0]
    for mi in mts[1:]:
        m = m.dot(mi)
    return m
```

Then $rtRRtr = ttt$

This is analog of zig-zag way, which is shorter than the straight line.

```python
#Checking this:
dots(r,t,R,R,t,r)
```

    array([[1, 0, 3],
           [0, 1, 0],
           [0, 0, 1]])

```python
r.dot([1,0,1])
```

    array([2, 1, 1])

```python
R.dot([1,0,1])
```

    array([ 1, -1,  1])

Translation up, by (0,1) is given by $RTrt$:

```python
dots(R,T,r,t)
```

    array([[1, 0, 0],
           [0, 1, 1],
           [0, 0, 1]])

And generally, by (0,n) is $RT^nrt^n$:

```python
dots(R,T,T,T,r,t,t,t)
```

    array([[1, 0, 0],
           [0, 1, 3],
           [0, 0, 1]])

Neighborhood order for a cell is, then, the minimal number T and t steps in the path to the cell.
Is it possible to apply knuth-bendix algorhythm to make convergent rewrite system?

Starting rules:

* $rtRRtr = ttt$
* $rR = Rr = Tt = tT = e$

Experiments with my older knuth_bendix implementation in Python show no success though.

# Effective neighbor calculation algorhithm

The algorhyths is not tested, but I am almost sure in it.

Idea: **simplest paths**
Assume that a lattice, with elementary skew $R$ is defined, and additionally, an *elementary translation* $T$ is defined (translation from the cell to the nearest neighbor. Usually, $T$ is translation by 1 cell. Then any larger translation can be obtained as superposition of transformations $R,R^{-1},T,T^{-1}$, called *path*. Two paths are equivalent if they describe the same transformation. Of all equivalent paths, **simplest paths** are the paths with minimal total number of $T,T^{-1}$ steps.

For example, on the 2111 lattice, let $T$ is translation by $(1,0)$. Then paths $TTT$ and $RTR^{-2}TR$ are equivalent, of them the latter is simpler. It is one of the simplest paths for translation $(3,0)$ (another one is $R^{-1}TR^2R^{-1}$).

Let's call the number of $T,T^{-1}$ steps in the simplest path **complexity** of the translation. Then neighbors are separated by translation of complexity 1. Order-2 neighbors have translation of complexity 2 etc.

Translation complexity behaves as true metric, triangle inequality holds for it.

## Calculating translation complexity

Any translation $X$ can be represented as superposition of powers of rotated elementary translations:

$$X = ... (R^{-2} T^{a_{-2}} R^2) (R^{-1} T^{a_{-1}} R) T^{a_0}(R T^{a_1} R^{-1})(R^2 T^{a_2} R^{-2})... $$

Where each power of $T$ present not more than once. Due to linear properties of translations, order can be arbitrary:

$$ (R^aT^nR^{-a})(R^bT^mR^{-b}) = (R^bT^mR^{-b})(R^aT^nR^{-a}) $$

Thus, any translation $X$ can be represented as a finite set of tuples $(n, a_n)$ for each nonzero $a_n$, or as a two-way infinite table of $a_n$ values with finite number of nonzero cells:

$n$ : | ... | -2 | -1 | 0 | 1 | 2 | ...
----|-----
$a_n$ :| ...| ${a_{-2}}$ | ${a_{-1}}$ | ${a_0}$ | ${a_1}$ | ${a_2}$ | ...

There are many equivalent representations for each translation. For example, the above equality says that transoation $(a_0=3)$ is equivalent to translation $(a_1=1)(a_{-1}=1)$.

Complexity of translation is:
$$C = \sum_{i=-\infty}^{\infty}|a_i| $$

Equivalent translations are given by the characteristic equation of the $R$ matrix:

```python
np.poly([[2,1],[1,1]])
```

    array([ 1., -3.,  1.])

$$R^2-3R+R=0$$

This means that adding $(k,-3k,k)$, where $k$ is any integer number, to any 3 consequent cells in the $a_i$ table does not change translation. Fro example, let's ensure that $(a_0,3) = (a_1=1, a_{_1}=1)$:

$a_{-1}$ | $a_0$ | $a_1$
----|---
0 | 3 | 0
+1 | -3 | +1
*1* | *0* | *1*

## Simplest path calculation

[QUestion on math overflow](https://math.stackexchange.com/questions/2537017/how-to-find-the-simplest-form-of-integer-matrix-polynomial)

THe most important idea: <s>**simple greedy algorhythm is enough to minimize complexity** . In other words: add $(k, -3k, k)$ triples to the table cells until it reduces $\sum|a_i|$. This is true, because $C=\sum |a_i|$ is a *convex function* of coefficients $k$.</s>

Experiment shows that this **idea fails**. There are local minima, where the process can stuck. For example, for polynomial $(1,-3,1)$, path $(2,1,1,2)$ is irreducible, but can be reduced to $(1,0,0,0,0,1)$.

This gives an effective way to calculate translation complexity and find common neighbors for 2 cells: if translation complexity is 2, then they have 2 common neighbors for each of the non-zero $a_i$.

```python
def charpoly2(m):
    #integer version of the 2x2 characteristic polynomial. starts from lower powers
    (a,b),(c,d) = m
    return [a*d-b*c, -a-d, 1]

assert charpoly2([[2,1],[1,1]]) == [1,-3,1]

def simplify_greedy(path, charpoly):
    """path: dict[int->int]. maps n to nonzero a_n. zeros are not in the map.
    """
    path = path.copy()
    def offsets(path):
        visited = set()
        for i in path.keys():
            for di in range(-len(charpoly), 0):
                i1 = i + di
                if i1 not in visited:
                    visited.add(i1)
                    yield i1

    def sumabs(s): return sum(abs(xi) for xi in s)

    def minblock(blk):
        ratios = [divmod(bi,pi) for bi, pi in zip(blk, charpoly) if pi!=0]
        def corners():
            for (r, rem) in ratios:
                if rem == 0:
                    yield r
                else:
                    yield r
                    yield r+1

        variants = [ [bi-rr*pi for bi, pi in zip(blk, charpoly)]
                     for rr in set(corners())
                   ]
        return min(variants, key = sumabs)

    need_continue = True
    while need_continue:
        need_continue = False
        for i0 in offsets(path):
            block = [path.get(i0+i, 0) for i in range(len(charpoly))]
            block1 = minblock(block)
            if block1 == block:
                #no minimization
                continue
            need_continue = True
            #write block back
            for i, new_bi in enumerate(block1):
                if new_bi == 0:
                    if (i0 + i) in path:
                        del path[i0+i]
                else:
                    path[i0+i] = new_bi
            break
    return path

```

```python
simplify_greedy({0:3}, [1,-3,1])
```

    {-1: 1, 1: 1}

```python
simplify_greedy({0:3000}, [1,-3,1])
```

    {-8: 1,
     -6: 2,
     -4: 2,
     -3: 2,
     -2: 2,
     -1: 1,
     0: 2,
     1: 1,
     2: 2,
     3: 2,
     4: 2,
     6: 2,
     8: 1}

```python
def path2xy(path, m, t0=np.array([1,0], dtype=np.int64)):
    xy = np.array([0,0], dtype=np.int64)
    for power, translation in path.items():
        mp = np.linalg.matrix_power(m,power).astype(np.int64).dot(t0)
        xy += mp * int(translation)
    return xy
def xy2path(xy, m, t0=np.array([1,0], dtype=np.int64)):
    mt0 = m.dot(t0)
    tt=np.stack((t0, mt0)).T
    itt = np.linalg.inv(tt)
    #check that it is integer
    if not (np.abs(itt - np.round(itt)) < 1e-5 ).all():
        print(itt)
        raise ValueError("Inverse transform is not integer, what to do?")
    itt = itt.astype(np.int64)
    k0, k1 = itt.dot(xy)
    p = {}
    if k0 != 0: p[0]=k0
    if k1 != 0: p[1]=k1
    return p
```

```python
M = np.array([[2,1],[1,1]], dtype=np.int64)

xy0 = np.linalg.matrix_power(M,3).dot([1,0]) + np.linalg.matrix_power(M,-2).dot([1,0])
xy0 = xy0.astype(np.int64)
print("XY original", xy0)

p = xy2path(xy0, M)
mp = simplify_greedy(p, charpoly2(M))
p0 = {3:1, -2:1}
print("Path:",p, "simple",mp)
xy1 = path2xy(p, M)
xy2 = path2xy(mp, M)
xy01 = path2xy(p0, M)
print("XY:",xy1, "simplified:",xy2, "p0:", xy01)
```

    XY original [15  5]
    Path: {0: 5, 1: 5} simple {0: 1, -1: 2, 2: 2, 1: 1}
    XY: [15  5] simplified: [15  5] p0: [15  5]

#### Greedy algorithm fails
Simplest example: (5,5|).  simplifies to (2,1,1|,2), but simplest is (1,0,0,0|,0,1)

### Plot how value depends on k

```python
fun = np.vectorize(lambda x: sum(abs(bi-pi*x) for bi, pi in zip([0,10,0],[1,-3,1])))
plt.plot(range(-5,5), fun(range(-5,5)), "bo", np.linspace(-5,5,100), fun(np.linspace(-5,5,100)), 'g.')
plt.show()
```

### String rewrite approach (failed)

Using greedy rewriter, it is alwayspossible to reduce string representation of the path to a string ov values in the range \[-2..2\] (higher values can always be reduced). This is true for other reducers, with n higher than 3.

What could be done further? Let's make a rewrite table

```python
def make_final_rewrite_table(n):
    p = (1, -n, 1)
    def weight(s): return sum(map(abs, s))
    def add_p(s, k):
        return tuple( si+k*pi for si,pi in zip(s, p) )
    def rewrite(s):
        return min((add_p(s,k) for k in range(-n,n+1)),
                  key = weight)
    table = {}
    for a in range(-n+1,n):
        for b in range(-n+1,n):
            for c in range(-n+1,n):
                s = (a,b,c)
                rs = rewrite(s)
                if rs!=s :
                    table[s] = rs
    return table
```

```python
make_final_rewrite_table(3)
```

    {(-2, -2, 1): (-3, 1, 0),
     (-2, -2, 2): (-3, 1, 1),
     (-2, 1, -2): (-1, -2, -1),
     (-2, 1, -1): (-1, -2, 0),
     (-2, 2, -2): (-1, -1, -1),
     (-2, 2, -1): (-1, -1, 0),
     (-2, 2, 0): (-1, -1, 1),
     (-2, 2, 1): (-1, -1, 2),
     (-2, 2, 2): (-1, -1, 3),
     (-1, -2, 1): (-2, 1, 0),
     (-1, -2, 2): (-2, 1, 1),
     (-1, 1, -2): (0, -2, -1),
     (-1, 1, -1): (0, -2, 0),
     (-1, 2, -2): (0, -1, -1),
     (-1, 2, -1): (0, -1, 0),
     (-1, 2, 0): (0, -1, 1),
     (-1, 2, 1): (0, -1, 2),
     (-1, 2, 2): (0, -1, 3),
     (0, -2, 1): (-1, 1, 0),
     (0, -2, 2): (-1, 1, 1),
     (0, 2, -2): (1, -1, -1),
     (0, 2, -1): (1, -1, 0),
     (1, -2, -2): (0, 1, -3),
     (1, -2, -1): (0, 1, -2),
     (1, -2, 0): (0, 1, -1),
     (1, -2, 1): (0, 1, 0),
     (1, -2, 2): (0, 1, 1),
     (1, -1, 1): (0, 2, 0),
     (1, -1, 2): (0, 2, 1),
     (1, 2, -2): (2, -1, -1),
     (1, 2, -1): (2, -1, 0),
     (2, -2, -2): (1, 1, -3),
     (2, -2, -1): (1, 1, -2),
     (2, -2, 0): (1, 1, -1),
     (2, -2, 1): (1, 1, 0),
     (2, -2, 2): (1, 1, 1),
     (2, -1, 1): (1, 2, 0),
     (2, -1, 2): (1, 2, 1),
     (2, 2, -2): (3, -1, -1),
     (2, 2, -1): (3, -1, 0)}

Does not work... It produces 3s and any attempt to avoid them give infinite ruleset.

### Another idea: "pull and split"

ANy value is represented as a pair of neighbor terms initially. Using the fact that (1,-3,1) pattern has 1s in the ends, it is possible to pull this nonzero pair to the left or to the right indefinitely:

```
0 0    0  5 5 0 0 0 0
0 0   -5 20 0 0 0 0 0   #-5*(1 -3 1)
0 -20 55  0 0 0 0 0 0
```
If initial pair is $(a,b)$, then pulled left it becomes $(-b, a+3b)$, and to the right: $(a+3b, -b)$. Thus, pulling is equivalent to multiplication by $\left(\begin{matrix}0&-1\\1&3\end{matrix}\right)$

Eigenvalues of this matrix are the same as of M, since determinant is 1 and trace is 3.

```python
#Example:
Left = np.array([[0,-1],[1,3]])
Left.dot(Left.dot([5,5]))
```

    array([-20,  55])

Using pull, it should be possible to find the local minimum of complexity.
However, how to split the value?

Let's try it.

```
0 0  0  0  5 5 0 0 0 0
0 0 -5 15  0 5 0 0 0 0
```
Now we have 2 groups: -5,15 and 0,5. Hm, nothing good.
```
0 0  0  0  5  5 0 0 0 0
0 0  0  2  -1 7 0 0 0 0
0 0  0  2  1  1 2 0 0 0
```
here, (21) and (12) groups.

```python
Left.dot(Left.dot([2,1]))
```

    array([-5, 14])

Impossible to pull them... and what if added 1 -2 -2 1 to the center?
```
0 0  0  0  5  5 0 0 0 0
0 0  0  2  1  1 2 0 0 0
0 0  0  3 -1 -1 3 0 0 0
```

```python
Left.dot([3,-1])
```

    array([1, 0])

### Hmm, another idea: get rid of 2s
Apply (1,-3,1) addition until there are no 2s. Should be possible?
this would generate suboptimal rewrite for (2): (1,-1,1). However, such cases can be restored by post-processing.

```python
def simplify_no2(path, charpoly):
    """path: dict[int->int]. maps n to nonzero a_n. zeros are not in the map.
    """
    path = path.copy()

    def minblock(blk):
        r, rem = divmod(blk[1], charpoly[1]) #by 3
        if rem == 0:
            corners = (r,)
        else:
            corners = (r, r+1)

        variants = [ [bi-rr*pi for bi, pi in zip(blk, charpoly)]
                     for rr in corners
                   ]
        return min(variants, key = lambda abc: abs(abc[1]))

    need_continue = True
    while need_continue:
        need_continue = False
        for i0_, val in path.items():
            if abs(abs(val)-abs(charpoly[1])) >= abs(val):
                continue
            i0 = i0_ - 1

            block = [path.get(i0+i, 0) for i in range(len(charpoly))]
            block1 = minblock(block)
            #print(f"orig block {block}, rewrite to {block1}")
            if block1 == block:
                #no minimization
                continue
            need_continue = True
            #write block back
            for i, new_bi in enumerate(block1):
                if new_bi == 0:
                    if (i0 + i) in path:
                        del path[i0+i]
                else:
                    path[i0+i] = new_bi
            break
    return path
```

Let's try it...

```python
M = np.array([[2,1],[1,1]], dtype=np.int64)

xy0 = np.linalg.matrix_power(M,3).dot([1,0]) - np.linalg.matrix_power(M,-10).dot([1,0]) + np.linalg.matrix_power(M,-2).dot([1,0])
xy0 = xy0.astype(np.int64)
print("XY original", xy0)

p = xy2path(xy0, M)
mp = simplify_no2(p, charpoly2(M))
print("Path:",p, "simple",mp)
xy1 = path2xy(p, M)
xy2 = path2xy(mp, M)
print("XY:",xy1, "simplified:",xy2)
```

    XY original [-4166  6770]
    Path: {0: -17706, 1: 6770} simple {4: 1, 2: -1, 1: -1, 3: -1, -1: -1, 0: -1, -10: -1, -3: 1, -2: -1}
    XY: [-4166  6770] simplified: [-4166  6770]

Hmmm, it seems to be **working**!

```python
p = simplify_no2(xy2path([1000,0], M), charpoly2(M))
print(p)
pp = simplify_greedy(p, charpoly2(M))
print(pp)
```

    {-3: -1, 3: -1, -7: 1, -5: 1, 5: 1, 7: 1, -1: -1, 1: -1, -4: 1, 4: 1, -2: 1, 0: 1, 2: 1}
    {-3: 2, 3: -1, -7: 1, -5: 1, 5: 1, 7: 1, -1: -1, 1: 2, 4: 1}

```python
def simplify(p, cp):
    return simplify_greedy(simplify_no2(p, cp),cp)
```

No, **still failing**:

```python
p = xy2path([-4166,6770], M)
print(p)
simplify(p, charpoly2(M))
```

    {0: -17706, 1: 6770}

    {-10: -1, -3: 1, -2: -1, -1: -1, 0: -1, 1: -1, 2: -1, 3: -1, 4: 1}

# Newton method approach

Consider equations system

$m=((-2,1),(1,2))$

$$\begin{cases} -x^2+xy+y^2=1 \\ -(x-x_0)^2+(x-x_0)(y-y_0)+(y-y_0)^2=1 \end{cases} $$

maxima code
```
pd(x,y) := (-x^2+y^2+xy-1);

e1 : pd(x,y);
e2 : pd(x0-x, y0-y);

J : matrix( [diff(e1,x), diff(e1,y)], [diff(e2,x), diff(e2,y)] );

dj : determinant(J);

IJ : ratsimp(invert(J) * dj);

dv : ratsimp(- IJ . matrix([e1],[e2]) / dj);```

Resutls are not very easy to use...

```python

```


# Magnetospheric Wald `GiRaFFEfood` Initial Data for `GiRaFFE`

## Author: Patrick Nelson

### NRPy+ Source Code for this module: [GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Magnetospheric_Wald.py](../../edit/in_progress/GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Magnetospheric_Wald.py)

**Notebook Status:**  Validated

**Validation Notes:** This tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation1). The initial data has validated against the original `GiRaFFE`, as documented [here](Tutorial-Start_to_Finish_UnitTest-GiRaFFEfood_NRPy.ipynb).

## Introduction:
With the `GiRaFFE` evolution thorn constructed, we now need to "feed" our giraffe with initial data to evolve. There are several different choices of initial data we can use here; here, we will only be implementing the "Magnetospheric Wald" initial data, given by Table 3 in [the original paper](https://arxiv.org/pdf/1704.00599.pdf):
\begin{align}
A_{i} &= \frac{C_0}{2} (g_{i \phi} + 2 a g_{t i}) \\
E_{i} &= 0 \\
\end{align}
(the unspecified components are set to 0). Here, $C_0$ is a constant that we will set to $1$ in our simulations. Now, to use this initial data scheme, we need to transform the above into the quantities actually tracked by `GiRaFFE` and HydroBase: $A_i$, $B^i$, $\tilde{S}_i$, $v^i$, and $\Phi$. Of these quantities, `GiRaFFEfood` will only set $A_i$, $v^i$, and $\Phi=0$, then call a separate function to calculate  $\tilde{S}_i$; `GiRaFFE` itself will call a function to set $B^i$ before the time-evolution begins. This can be done with eqs. 16 and 18, here given in that same order:
\begin{align}
v^i &= \alpha \frac{\epsilon^{ijk} E_j B_k}{B^2} -\beta^i \\
B^i &= \frac{[ijk]}{\sqrt{\gamma}} \partial_j A_k \\
\end{align}
In the simulations, $B^i$ will be calculated numerically from $A_i$; however, it will be useful to analytically calculate $B^i$ to use calculating the initial $v^i$.

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

This notebook is organized as follows

1. [Step 1](#initializenrpy): Import core NRPy+ modules and set NRPy+ parameters
1. [Step 2](#set_a_i): Set the vector $A_i$
1. [Step 3](#set_vi): Calculate $v^i$ from $A_i$ and $E_i$
1. [Step 4](#code_validation1): Code Validation against `GiRaFFEfood_NRPy.GiRaFFEfood_NRPy` NRPy+ Module
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Import core NRPy+ modules and set NRPy+ parameters \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

Here, we will import the NRPy+ core modules, set the reference metric to Cartesian, and set commonly used NRPy+ parameters. We will also set up a parameter to determine what initial data is set up, although it won't do much yet.

```python
# Step 0: Add NRPy's directory to the path
# https://stackoverflow.com/questions/16780014/import-file-from-parent-directory
import os,sys
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

# Step 0.a: Import the NRPy+ core modules and set the reference metric to Cartesian
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import GiRaFFEfood_NRPy.GiRaFFEfood_NRPy_Common_Functions as gfcf # Some useful functions for GiRaFFE initial data.

import reference_metric as rfm
par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
rfm.reference_metric()

# Step 1a: Set commonly used parameters.
thismodule = "GiRaFFEfood_NRPy_Magnetospheric_Wald"

C0 = par.Cparameters("REAL",thismodule,"C0",1.0)
```

# Step 2: Set the vector $A_i$ \[Back to [top](#toc)\]
$$\label{set_a_i}$$

We will first build the fundamental vectors $A_i$ and $E_i$ in spherical coordinates (see [Table 3](https://arxiv.org/pdf/1704.00599.pdf)). Note that we use reference_metric.py to set $r$ and $\theta$ in terms of Cartesian coordinates; this will save us a step later when we convert to Cartesian coordinates. Since $C_0 = 1$,
$$A_{i} = \frac{C_0}{2} (g_{i \phi} + 2 a g_{t i}).$$

```python
# Step 2: Set the vectors A and E in Spherical coordinates
def Ar_MW(r,theta,phi, **params):
    g4DD = params["g4DD"]
    a    = params["a"]
    return sp.Rational(1,2) * C0 * (g4DD[1][3] + 2 * a * g4DD[0][1])

def Ath_MW(r,theta,phi, **params):
    g4DD = params["g4DD"]
    a    = params["a"]
    return sp.Rational(1,2) * C0 * (g4DD[2][3] + 2 * a * g4DD[0][2])

def Aph_MW(r,theta,phi, **params):
    g4DD = params["g4DD"]
    a    = params["a"]
    return sp.Rational(1,2) * C0 * (g4DD[3][3] + 2 * a * g4DD[0][3])
```

# Step 3: Calculate $v^i$ from $A_i$ and $E_i$ \[Back to [top](#toc)\]
$$\label{set_vi}$$

Next, we will code up the Valencia velocity, which will require us to first code the electric and magnetic fields.

The electric field is
$$E_{i} = 0$$\
Since $$v_{(n)}^i = \frac{\epsilon^{ijk} E_j B_k}{B^2},$$ we trivially find $$v_{(n)}^i = 0,$$

```python
#Step 3: Compute v^i from A_i and E_i
def ValenciavU_func_MW(**params):
    return ixp.zerorank1()
```

# Step 4: Code Validation against `GiRaFFEfood_NRPy.GiRaFFEfood_NRPy` NRPy+ module  \[Back to [top](#toc)\]
$$\label{code_validation1}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for the `GiRaFFE` Exact Wald initial data equations  we intend to use between
1. this tutorial and
2. the NRPy+ [GiRaFFEfood_NRPy.GiRaFFEfood_NRPy_Exact_Wald](../edit/GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Exact_Wald.py) module.

```python
import GiRaFFEfood_NRPy.GiRaFFEfood_NRPy as gf
import BSSN.ShiftedKerrSchild as sks
sks.ShiftedKerrSchild(True)
import reference_metric as rfm   # NRPy+: Reference metric support
par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
rfm.reference_metric()

import BSSN.ADMBSSN_tofrom_4metric as AB4m
AB4m.g4DD_ito_BSSN_or_ADM("ADM",sks.gammaSphDD,sks.betaSphU,sks.alphaSph)
g4DD = AB4m.g4DD

for mu in range(4):
    for nu in range(4):
        g4DD[mu][nu] = g4DD[mu][nu].subs(sks.r,rfm.xxSph[0]).subs(sks.th,rfm.xxSph[1])

A_mwD = gfcf.Axyz_func_spherical(Ar_MW,Ath_MW,Aph_MW,stagger_enable = True,a=sks.a,g4DD = g4DD)
Valenciav_mwD = ValenciavU_func_MW()
gf.GiRaFFEfood_NRPy_generate_initial_data(ID_type = "MagnetosphericWald", stagger_enable = True,a=sks.a,g4DD = g4DD)

print("Consistency check between GiRaFFEfood_NRPy tutorial and NRPy+ module:")

def consistency_check(quantity1,quantity2,string):
    if quantity1-quantity2==0:
        print(string+" is in agreement!")
    else:
        print(string+" does not agree!")
        sys.exit(1)

for i in range(3):
    consistency_check(Valenciav_mwD[i],gf.ValenciavU[i],"ValenciavU"+str(i))
    consistency_check(A_mwD[i],gf.AD[i],"AD"+str(i))
```

    Consistency check between GiRaFFEfood_NRPy tutorial and NRPy+ module:
    ValenciavU0 is in agreement!
    AD0 is in agreement!
    ValenciavU1 is in agreement!
    AD1 is in agreement!
    ValenciavU2 is in agreement!
    AD2 is in agreement!

# Step 5: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

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

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-GiRaFFEfood_NRPy-Magnetospheric_Wald",location_of_template_file=os.path.join(".."))
```

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


```python
from sympy import *
from sympy.abc import *
from sympy.galgebra.ga import *
import numpy as np
from numpy import linalg as LA
from __future__ import print_function
init_printing()
```

# Multi order stencil for the 2D/3D acoustic isotropic wave equation

```python
# Choose dimension (2 or 3)
dim = 2
# Choose order
time_order = 6
space_order = 12

# half width for indexes, goes from -half to half
width_t = int(time_order/2)
width_h = int(space_order/2)
```

```python
# Define functions and symbols
p=Function('p')
s,h = symbols('s h')
if dim==2:
    m=M(x,z)
    q=Q(x,z,t)
    d=D(x,z,t)
    solvep = p(x,z,t+width_t*s)
    solvepa = p(x,z,t-width_t*s)
else :
    m=M(x,y,z)
    q=Q(x,y,z,t)
    d=D(x,y,z,t)
    solvep = p(x,y,z,t+width_t*s)
    solvepa = p(x,y,z,t-width_t*s)
```

```python
# Finite differences coefficients, not necessary here but good to have somewhere
def fd_coeff_1(order):
    if order==16:
        coeffs = [0.000010, -0.000178, 0.001554, -0.008702, 0.035354, -0.113131, 0.311111, -0.888889, 0.000000,
                  0.888889, -0.311111, 0.113131, -0.035354, 0.008702, -0.001554, 0.000178, -0.000010]

    if order==14:
        coeffs = [-0.000042, 0.000680, -0.005303, 0.026515, -0.097222, 0.291667, -0.875000, 0.000000,
                  0.875000, -0.291667, 0.097222, -0.026515, 0.005303, -0.000680, 0.000042]

    if order==12:
        coeffs = [ 0.0002, -0.0026, 0.0179, -0.0794, 0.2679, -0.8571, -0.0000, 0.8571, -0.2679, 0.0794, -0.0179, 0.0026, -0.0002]

    if order==10:
        coeffs =[-0.0008 , 0.0099, -0.0595, 0.2381, -0.8333, 0.0000, 0.8333, -0.2381, 0.0595, -0.0099, 0.0008]

    if order==8:
        coeffs = [1.0/280, -4.0/105, 1.0/5, -4.0/5, 0, 4.0/5, -1.0/5,4.0/105,-1.0/280]

    if space_order==6:
        coeffs = [-1.0/60, 3.0/20, -3.0/4, 0, 3.0/4, -3.0/20, 1.0/60]

    if space_order==4:
        coeffs = [1.0/12, -2.0/3, 0, 2.0/3, -1.0/12]

    if space_order==2:
        coeffs = [-0.5, 0, 0.5]

def fd_coeff_2(ordrer):
    if order==16:
        coeffs = [-0.000002, 0.000051, -0.000518, 0.003481, -0.017677, 0.075421, -0.311111, 1.777778, -3.054844,
                  1.777778, -0.311111, 0.075421, -0.017677, 0.003481, -0.000518, 0.000051, -0.000002]

    if order==14:
        coeffs = [0.000012, -0.000227, 0.002121, -0.013258, 0.064815, -0.291667, 1.750000, -3.023594,
                  1.750000, -0.291667, 0.064815, -0.013258, 0.002121, -0.000227, 0.000012]

    if order==12:
        coeffs = [-0.000060, 0.001039, -0.008929, 0.052910, -0.267857, 1.714286, -2.982778,
                  1.714286, -0.267857, 0.052910, -0.008929, 0.001039, -0.000060]

    if order==10:
        coeffs = [0.000317, -0.004960, 0.039683, -0.238095, 1.666667, -2.927222,
                  1.666667, -0.238095, 0.039683, -0.004960, 0.000317]

    if order==8:
        coeffs = [-0.001786, 0.025397, -0.200000, 1.600000, -2.847222, 1.600000, -0.200000, 0.025397, -0.001786]

    if space_order==6:
        coeffs = [0.011111, -0.150000, 1.500000, -2.722222, 1.500000, -0.150000, 0.01111]

    if space_order==4:
        coeffs = [-0.083333, 1.333333, -2.500000, 1.333333, -0.08333]

    if space_order==2:
        coeffs = [1, -2, 1]
```

```python
# Indexes for finite differences
indx = []
indy = []
indz = []
indt = []
for i in range(-width_h,width_h+1):
    indx.append(x + i * h)
    indy.append(y + i * h)
    indz.append(z + i* h)

for i in range(-width_t,width_t+1):
    indt.append(t + i * s)
```

```python
# Finite differences
if dim==2:
    dtt=as_finite_diff(p(x,z,t).diff(t,t),indt)
    dxx=as_finite_diff(p(x,z,t).diff(x,x), indx)
    dzz=as_finite_diff(p(x,z,t).diff(z,z), indz)
    dt=as_finite_diff(p(x,z,t).diff(t), indt)
    lap = dxx + dzz
else:
    dtt=as_finite_diff(p(x,y,z,t).diff(t,t),indt)
    dxx=as_finite_diff(p(x,y,z,t).diff(x,x), indx)
    dyy=as_finite_diff(p(x,y,z,t).diff(y,y), indy)
    dzz=as_finite_diff(p(x,y,z,t).diff(z,z), indz)
    dt=as_finite_diff(p(x,y,z,t).diff(t), indt)
    lap = dxx + dyy + dzz
```

```python
# Argument list for lambdify
arglamb=[]
arglamba=[]
if dim==2:
    for i in range(-width_t,width_t):
        arglamb.append( p(x,z,indt[i+width_t]))
        arglamba.append( p(x,z,indt[i+width_t+1]))

    for i in range(-width_h,width_h+1):
        for j in range(-width_h,width_h+1):
            arglamb.append( p(indx[i+width_h],indz[j+width_h],t))
            arglamba.append( p(indx[i+width_h],indz[j+width_h],t))
else:
    for i in range(-width_t,width_t):
        arglamb.append( p(x,y,z,indt[i+width_t]))
        arglamba.append( p(x,y,z,indt[i+width_t+1]))

    for i in range(-width_h,width_h+1):
        for j in range(-width_h,width_h+1):
            for k in range(-width_h,width_h+1):
                arglamb.append( p(indx[i+width_h],indy[i+width_h],indz[j+width_h],t))
                arglamba.append( p(indx[i+width_h],indy[i+width_h],indz[j+width_h],t))

arglamb.extend((q , m, s, h, e))
arglamb=tuple(arglamb)
arglamba.extend((q , m, s, h, e))
arglamba=tuple(arglamba)
```

```python
solvepa
```

# Forward and adjoint stencil
2D-3D is automatic from the setup

```python
# Forward wave equation
wave_equation = m*dtt- lap- q  + e*dt
stencil = solve(wave_equation,solvep)[0]
ts=lambdify(arglamb,stencil,"numpy")
stencil
```

```python
# Adjoint wave equation
wave_equationA = m*dtt- lap - d - e*dt
stencilA = solve(wave_equationA,solvepa)[0]
tsA=lambdify(arglamba,stencilA,"numpy")
stencilA
```

```python

```