scipy.integrate.solve_ivpで微分方程式をたくさん解いてみた



# 対象
数値計算を使って勉強している物理，化学系の学生向け


# はじめに
物理現象を記述する微分方程式を数値的に解く，みたいなのは理系専攻のカリキュラムなら，どこもやっていると思います．
とりあえず差分化してコードに直せば，解を出してくれて，グラフに直せばそのイメージが掴める，とても便利な方法です．

一方で面倒なこともあります．

一つがコードを書く手間です．
数値計算の講義ではCが主流かと思いますが，C言語は手軽に扱うには向きません．デバッグに時間もかかります．

また，刻み幅の調整も問題です．この値が大きすぎると正しい解が得られません，かといって，小さすぎると計算時間がかかりすぎます．

この二つの面倒さに対し，自分なりに解決を行いましたので，紹介することにしました


# pythonとscipy.integrate.solve_ivp

まず，言語としてPythonを採用します．pythonといえば遅い言語のイメージがありますが，数値計算ライブラリの拡充により，ちょっとした計算をやるときには個人的にほぼ一択です．まあ3次元シミュレーションとかになるときついんですが．

そして，pythonのscipy.integrateライブラリからsolve_ivpを用います．solve_ivpは，(下は公式の解説)，刻み幅の自動調節を行ってくれる手法をwrapした(連立)常微分方程式solverです．

https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html#scipy.integrate.solve_ivp

solve_ivpを用いてコードを書き，微分方程式を解いていきたいと思います．

物理的にも面白そうな例をたくさん用意しました．

scipy.integrate.solve_ivpと，計算結果も気に入っていただけると幸いです．



# 目次

1. [簡単なODE](#簡単なode)
- [非線形ODE](#非線形ode)
- [テンソルODE](#テンソルode)
- [1次元場PDE(基礎編)](#1次元場pde-基礎編) 
- [1次元場PDE(応用編)](#1次元場pde-応用編) 
- [2次元場PDE(基礎編)](#2次元場pde-基礎編)
- [2次元場PDE(応用編)](#2次元場pde-応用編)


# 簡単なODE


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
```

## 単一緩和の減衰挙動

まず，　単一緩和の減衰運動を解いてみましょう．
$$
    \frac{{\rm d}x}{{\rm d}t} = -x
$$


```python
init   = [1.0]
t_span = [0.0,5.0]
t_eval = np.linspace(*t_span,100) # time for sampling
def decay(t,x): return -x
sol = solve_ivp(decay,t_span,init,method='RK45',t_eval=t_eval)
```

solve_ivpでラップされたRK45法により，内部で精度を保証できるよう時間刻みを勝手に調整してくれます．嬉しいですね!


```python
plt.plot(sol.t,sol.y[0,:],'k,-')
plt.plot(sol.t,np.exp(-sol.t),'r,--')
plt.legend(['Numerical','Analytical'])
plt.show()
plt.semilogy(sol.t,np.abs(sol.y[0,:]-np.exp(-sol.t)),'r')
plt.legend(['abs error'])
```

結果
![1_simple_6_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/1d15fe75-6d29-6702-727c-59097046e725.png)

絶対誤差
![1_simple_6_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/5bf0902a-c300-132c-f283-433f1f090216.png)



## 1次元調和振動子

次に，1次元調和振動子を例に挙げます．
ハミルトニアンを書き下します．
$$
    H = \frac{p^2}{2} + \frac{q^2}{2}
$$
簡単のため定数は1としました．　正準方程式から連立ODEを得ます．
$$
    \dot p = -q\\
    \dot q = p
$$
勿論，連立ODEも簡単に扱うことができます．


```python
p0 = 1.0; q0 = 0.0
init   = [p0,q0]
t_span = [0.0,20.0]
t_eval = np.linspace(*t_span,100) # time for sampling
def halmonic(t,x): 
    p,q = x
    return [-q,p]
sol = solve_ivp(halmonic,t_span,init,method='RK45',t_eval=t_eval)
```


```python
plt.plot(sol.y[0,:],sol.y[1,:],'b,-')
plt.legend(['p,q-trace'])
plt.show()
plt.plot(sol.y[0,:]**2/2+sol.y[1,:]**2/2,'r,-')
plt.legend(['Hamiltonian'])
plt.show()
```

結果
![1_simple_10_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/51d953f4-c823-ff9d-5e8e-965f40ab4841.png)


ハミルトニアン
![1_simple_10_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/e4252c46-1882-0ddb-3f10-a9004c921286.png)


<a id="2"></a>
# 非線形ODE


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from mpl_toolkits.mplot3d import Axes3D
```

## ローレンツアトラクタ

次はローレンツアトラクタを描いてみましょう．
$$
    \frac{{\rm d}x}{{\rm d}t} = -px +py    ,
    \frac{{\rm d}y}{{\rm d}t} = -xz +rx -y ,
    \frac{{\rm d}z}{{\rm d}t} = -xy +bz
$$
ここで，$p=10, r=28, b=8/3$を使いました(wikipedia). パラメーターは引数argに追加することができます．


```python
init   = [1.0,0.0,0.0]
t_span = [0.0,50.0]
t_eval = np.linspace(*t_span,3000) # time for sampling
p=10.; r=28.; b=8./3.;
def lorenz(t,X,p,r,b): 
    x,y,z = X
    return [-p*x+p*y, -x*z+r*x-y, x*y-b*z]
sol = solve_ivp(lorenz,t_span,init,method='RK45',t_eval=t_eval,args=(p,r,b,))
```

綺麗なカオス的ふるまいが見れます．　

7/2追記)
許容誤差を調整しないと誤差が大きくなってしまうみたいですね．詳しくはこちらの記事を．
https://qiita.com/Sunset_Yuhi/items/d938718ad277eeab746c

```python
fig = plt.figure(); ax = Axes3D(fig)
ax.plot(sol.y[0,:],sol.y[1,:],sol.y[2,:],'k-',lw=0.5)
```

![2_nonlinear_6_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/69a9b23e-1518-edd3-f7ce-575fcaab0b05.png)



## SIRモデル

最近有名なやつです．
https://ja.wikipedia.org/wiki/%E7%96%AB%E5%AD%A6%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%8C%BA%E7%94%BB%E3%83%A2%E3%83%87%E3%83%AB

Sが感染しうる個体，Iが感染者，Rが免疫，死亡，位置的などの意味での抵抗獲得者を表します．

$$
\begin{eqnarray}
\frac{{\rm d}S}{{\rm d}t}&&=-\frac{\beta IS}{N}\\
\frac{{\rm d}I}{{\rm d}t}&&=+\frac{\beta IS}{N}-\gamma I\\
\frac{{\rm d}R}{{\rm d}t}&&=\gamma I\\
\end{eqnarray}
$$


```python
N = 100
init   = [N*0.9,N*0.05,N*0.05]
t_span = [0.0,10.0]
t_eval = np.linspace(*t_span,1000) # time for sampling
β = 1.5; γ = 0.1;
def lorenz(t,SIR,N,β,γ): 
    S,I,R = SIR
    return [-β*I*S/N, +β*I*S/N-γ*I, γ*I]
sol = solve_ivp(lorenz,t_span,init,method='RK45',t_eval=t_eval,args=(N,β,γ,))
```

基本再生産数$\beta/\gamma$を15にしています．

結構な感染爆発が起きるが，中々治らない．という感じになります．


```python
S,I,R = sol.y
plt.plot(S)
plt.plot(I)
plt.plot(R)
plt.legend(['S','I','R'])
```

![2_nonlinear_11_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/384f4b24-d2ee-2bb8-1adb-3323ea4f0584.png)


# テンソルODE


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp 
```

## Maxwellの応力挙動

連続体を扱うとき，２階のテンソルが頻出します．
例えばMaxwell流体の３次元構成関係を見てましょう，応力$\sigma_{ij}$の時間発展は，$\kappa_{ij}=\nabla_i v_j$は速度勾配，$G$は平衡圧として，以下の形で書けます．
$$
    \frac{{\rm d}\boldsymbol\sigma}{{\rm d} t} = 
    \boldsymbol\kappa\cdot\boldsymbol\sigma + 
    \boldsymbol\sigma\cdot\boldsymbol\kappa^T - (\boldsymbol \sigma-G\boldsymbol I)
$$
$ij$成分を陽に考えて実装するのは煩わしいですね．
でもnumpyを使えば右辺の計算を見た目そのままにコードに書くことができます．
solve_ivpに解いてもらいましょう.$xy$方向にせん断変形を与えます．


```python
dim = 3
σ = np.eye(dim)
κ = np.zeros_like(σ)
δ = np.eye(dim)
κ[0,1] = 1.0 # xy-shear
init   = σ.flatten()
t_span = [0.0,5.0]
t_eval = np.linspace(*t_span,100) # time for sampling
def maxwell(t,σ,κ):
    σ = σ.reshape(dim,dim)
    dσ = κ.dot(σ) + σ.dot(κ.T) - (σ - δ)
    return dσ.flatten()
sol = solve_ivp(maxwell,t_span,init,method='RK45',t_eval=t_eval,args=(κ,))
```

maxwell関数内で，受け取った$\sigma$を二次元にreshapeすること， 返り値を一次元で返す必要があることに注意します．

ギリシャ文字はjupyter labで打っています．　読むときに楽で良いですね．

成長せん断粘度$\eta_s = \sigma_{xy}/\kappa_{xy}$を見てましょう．


```python
plt.plot(sol.t,sol.y[1]/κ[0,1])
```

![3_tensor_6_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/086a8dc3-53ed-2bfc-840b-dc96eef4ad8e.png)


## Giesekusの応力挙動

Giesekus流体に変更してみましょう．　構成式は以下の形をしており．
$$
    \frac{{\rm d}\boldsymbol\sigma}{{\rm d} t} =
    \boldsymbol\kappa\cdot\boldsymbol\sigma +
    \boldsymbol\sigma\cdot\boldsymbol\kappa^T - (\boldsymbol \sigma-G\boldsymbol I) -
    \alpha(\boldsymbol\sigma - G\boldsymbol I)\cdot (\frac{\boldsymbol\sigma}{G} - \boldsymbol I)
$$
パラメータ$\alpha\in(0,1]$に依存する非線型項を持ちます．



```python
dim = 3
σ = np.eye(dim)
κ = np.zeros_like(σ)
δ = np.eye(dim)
α = 0.5
κ[0,1] = 1.0 # xy-shear
init   = σ.flatten()
t_span = [0.0,5.0]
t_eval = np.linspace(*t_span,100) # time for sampling
def giesekus(t,σ,κ,α):
    σ = σ.reshape(dim,dim)
    dσ = κ.dot(σ) + σ.dot(κ.T) - (σ - δ) - α * (σ - δ).dot(σ - δ)
    return dσ.flatten()
sol = solve_ivp(giesekus,t_span,init,method='RK45',t_eval=t_eval,args=(κ,α))
```

粘度のオーバーシュートと定常値の低減（シアシニング）を観察できます．


```python
plt.plot(sol.t,sol.y[1]/κ[0,1])
```



![3_tensor_11_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/078e8176-8263-4869-4689-872c1350c045.png)



# 1次元場PDE (基礎編)

時間だけでなく位置$x$に依存する偏微分方程式を考えてみましょう．　$x$に対する空間の差分化さえできれば．後は連立ODEと同じになります．


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp 
```

## 拡散方程式
熱拡散の問題を考えます．
一次元場の熱保存式は，次の形で書けます
$$
\frac{\partial \theta}{\partial t}= \alpha\frac{\partial^2 \theta}{\partial x^2}
$$
$x$の空間差分をとると．
$$
\frac{\partial \theta^n}{\partial t}= \alpha\frac{\theta^{n+1}-2\theta^{n}+\theta^{n-1}}{\Delta x^2}
$$
$\theta=1$ at $x=0,1$を境界として，$x$が$[0,1]$の範囲を計算します．


```python
nx = 100
x  = np.linspace(0.0,1.0,nx) 
θ  = np.zeros_like(x)
θ[0] = θ[-1] = 1.
Δx = 1.0/(nx-1)
α = 0.25
init   = θ
t_span = [0,1]
t_eval = np.linspace(*t_span,20) # time for sampling
def diffusion(t,θ,α,Δx):
    dθ = α * np.diff(θ,2)/(Δx**2)
    return np.hstack([0.0,dθ,0.0])
sol = solve_ivp(diffusion,t_span,init,method="RK45",t_eval=t_eval,args=(α,Δx))
```


```python
cmap = plt.get_cmap('jet')
Np = len(sol.t)
for i in range(Np):
    plt.plot(x,sol.y[:,i],c=cmap(i/Np))
```


![4_1D-field-fundamental_5_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/7db04aec-ad3f-f2c1-66aa-cbfb7c32940c.png)


## 移流方程式
矩形波の進行問題を考えてみましょう．
等速流一次元場の質量保存式は，次の形で書けます．
$$
\frac{\partial \rho}{\partial t}+ v_x\frac{\partial \rho}{\partial x}=0
$$

この差分の取り扱いは結構難しいです．ここでは$x$の２次後進差分をとり，許容相対誤差(rtol)を小さくしてみます．
$$
\frac{\partial \rho^n}{\partial t}= -v_x\frac{3\rho^{n}-4\rho^{n-1}+\rho^{n-2}}{2\Delta x}
$$

$v_x=1$を一定値として，$x$が$[0,1]$の範囲を考えます．初期条件は,$\rho(0.1<x<0.2)=1$　これ以外で$0$とします．


```python
nx = 1000
vx = 0.8
x  = np.linspace(0.0,1.0,nx) 
ρ  = np.where((0.1<x)&(x<0.2),1.0,0.0)
Δx = 1.0/(nx-1)
init   = ρ
t_span = [0,1]
t_eval = np.linspace(*t_span,6) # time for sampling
def advection(t,ρ,vx,Δx):
    dρ = -vx * np.convolve(ρ,[3.0,-4.0,1.0],'valid')/(2*Δx)
    return np.hstack([0.0,0.0,dρ])
sol = solve_ivp(advection,t_span,init,method="RK45",t_eval=t_eval,args=(vx,Δx),rtol=1e-5)
```

少し乱れているが，進行する様子が観察できます．ちなみに前進差分で計算すると簡単に発散し，大変なことになります(なった)．


```python
cmap = plt.get_cmap('jet')
Np = len(sol.t)
for i in range(Np):
    plt.plot(x,sol.y[:,i],c=cmap(i/Np))
```


![4_1D-field-fundamental_9_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/3e9f8434-d264-3b8d-0df1-1fbdc317ab3e.png)


## 波動方程式
時間方向の二階微分式中に登場する波動方程式を考えます．
$$
\frac{\partial^2 u}{\partial t^2} = c^2 \frac{\partial^2 u}{\partial x^2}
$$

ここで，ある$x$における$u=u^n$について$u, u'=\frac{\partial u}{\partial t}$に対する2本のODEが書けます

$$
\begin{eqnarray}
    \frac{\partial u }{\partial t}&&= u'\\
    \frac{\partial u'}{\partial t}&&= c^2\frac{u^{n+1}-2u+u^{n-1}}{\Delta x^2}
\end{eqnarray}
$$
正弦波形の時間発展を見てみましょう．



```python
nx = 100
x  = np.linspace(0.0,1.0,nx) 
u  = np.sin(2.0*np.pi*x)
du = np.cos(3.0*np.pi*x)
Δx = 1.0/(nx-1)
c2 = 0.05
init   = np.hstack([u,du])
t_span = [0,1]
t_eval = np.linspace(*t_span,50) # time for sampling
def wave(t,U,c2,Δx):
    u,du = U[:nx],U[nx:]
    u[0] = u[-1] = 0.0
    ddu = c2 * np.diff(du,2)/(Δx**2)
    return np.hstack([du,0.0,ddu,0.0])
sol = solve_ivp(wave,t_span,init,method="RK45",t_eval=t_eval,args=(c2,Δx))
```

u[0..n-1],du[0..n-1]をまとめて扱うのがコツです．
上の画像は$u$，下の画像は$u'$の時間発展を表しています．


```python
fig,ax = plt.subplots()
ax.imshow(sol.y.T[:,:nx],cmap='jet')
fig,ax = plt.subplots()
ax.imshow(sol.y.T[:,nx:],cmap='jet')
```



![4_1D-field-fundamental_13_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/6a6245fc-e02a-ed28-1054-5c198b571d8b.png)


![4_1D-field-fundamental_13_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/2a10639d-21e6-0489-3bc0-ce324f2b0fac.png)



# 1次元場PDE (応用編)

## Burgers方程式


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp 
```

ナビエストークス方程式の移流項$u\partial_x u$と粘性項$\partial_{xx} u$のみを考える形です．
$$
    \partial_t u + \lambda_1 u\partial_x u = \lambda_2 \partial_{xx} u
$$

移流拡散方程式とも呼ばれています．


```python
# parameters
nx = 100
Δx = 1.0/(nx-1)
λ1 = 1.0; λ2 = 0.1; 
# init
x  = np.linspace(0.0,1.0,nx) 
u  = np.zeros_like(x)
u[10:30]  = 1.0
init   = u
# time
t_span = [0.,0.5]
t_eval = np.linspace(*t_span,50) # time for sampling
# ODEs
def ODEs(t,u,λ1,λ2,Δx):
    du = - λ1 * (np.diff(u)/(Δx))[1:] + λ2 * np.diff(u,2)/(Δx**2)
    return np.hstack([0.0,du,0.0])
# solve
sol = solve_ivp(ODEs,t_span,init,method="RK45",t_eval=t_eval,args=(λ1,λ2,Δx))
```

進行しながら滑らかになる様子が見れる．


```python
fig,ax = plt.subplots()
ax.imshow(sol.y.T,cmap='jet')
```

![5_1D-field-advance_6_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/76e85877-e3b0-d908-863c-e403baa5dccc.png)




## KdV方程式

コルトヴェーグと，ド・フリースにより定式化された．非線形波を表す方程式です．
$$
    \partial_t u + 6u \partial_x u + \partial_{xxx}u=0
$$

パラメータは，この論文を参考にさせていただきました．
http://advances.sciencemag.org/content/3/4/e1602614


```python
# parameters
nx = 200
Δx = 60./(nx-1)
# init
x  = np.linspace(0.0,60.0,nx) 
u  = np.exp(-((x-10.0)/(5.0))**2)
init   = u
# time
t_span = [0.,20.0]
t_eval = np.linspace(*t_span,100) # time for sampling
# ODEs
def ODEs(t,u,Δx):
    du = - 6 * u[2:-2] * np.convolve(u,[1.,0.,-1.],'valid')[1:-1]/(2*Δx) - np.diff(u,3)[1:]/(Δx**3)
    return np.hstack([0.,0.,du,0.,0.])
# solve
sol = solve_ivp(ODEs,t_span,init,method="RK45",t_eval=t_eval,args=(Δx,),rtol=1e-8)
```

正規分布の初期速度を置いた．時間の進行とともに幾つかの孤立波になっています．


```python
fig,ax = plt.subplots()
ax.imshow(sol.y.T,cmap='jet')
```

![5_1D-field-advance_11_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/74cc6b3a-9848-4549-5d91-9dd097e7fae3.png)


## KS方程式

蔵元ーシバシンスキー方程式．火砕流の挙動を表すよう定式化されました．4階微分が特徴的．拡散の強さが拡散します．
$$
    \partial_t u + u\partial_x u + \partial_{xx} u + \partial_{xxxx} u = 0
$$


```python
# parameters
nx = 200
Δx = 100./(nx-1)
# init
x  = np.linspace(0.0,60.0,nx) 
u  = np.exp(-((x-40.0)/(5.0))**2) + np.exp(-((x-60.0)/(5.0))**2)
init   = u
# time
t_span = [0.,100.0]
t_eval = np.linspace(*t_span,100) # time for sampling
# ODEs
def ODEs(t,u,Δx):
    du = - u[2:-2] * np.convolve(u,[1.,0.,-1.],'valid')[1:-1]/(2*Δx)\
         - np.diff(u,2)[1:-1]/(Δx**2)\
         - np.diff(u,4)      /(Δx**4)
    return np.hstack([0.,0.,du,0.,0.])
# solve
sol = solve_ivp(ODEs,t_span,init,method="RK45",t_eval=t_eval,args=(Δx,),rtol=1e-8)
```

インパクトあって目に楽しい図ができます


```python
fig,ax = plt.subplots()
ax.imshow(sol.y.T,cmap='jet')
```



![5_1D-field-advance_16_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/d9e71417-efbe-7b4a-cceb-15f96e525261.png)




## Schorodinger方程式

理系大学生の関門として名高いシュレディンガー方程式．数値計算を通してイメージを掴んでみましょう．
一次元では，定数を1，二次関数型のポテンシャル場$V(x)=x^2/2$を考えると以下の形で書けます．
$$
    i\partial_t \Psi = -\left(\frac{1}{2}\partial_{xx}- \frac{x^2}{2}\right)\Psi
$$

ちなみにsolve_ivpはcomplexもrealと変わらず扱えます．すごい!


```python
# parameters
nx = 200
Δx = 15./(nx-1)
# init
x  = np.linspace(-7.5,7.5,nx) 
Ψ  = 2.*np.exp(-x**2) + 0.j
init   = Ψ
# time
t_span = [0.,20.0]
t_eval = np.linspace(*t_span,400) # time for sampling
# ODEs
def ODEs(t,Ψ,Δx):
    dΨ = -1.j*(- 0.5*np.diff(Ψ,2)/(Δx**2) + 0.5*(x**2*Ψ)[1:-1])
    return np.hstack([0.+0.j,dΨ,0.+0.j])
# solve
sol = solve_ivp(ODEs,t_span,init,method="RK45",t_eval=t_eval,args=(Δx,),rtol=1e-8)
```

ポテンシャルによって電子波が中心にトラップされています．


```python
fig,ax = plt.subplots()
ax.imshow(sol.y[:].real,cmap='jet')
fig,ax = plt.subplots()
ax.imshow(sol.y[:].imag,cmap='jet')
```

![5_1D-field-advance_21_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/a844f1a0-f766-de68-24bc-4f644955f0d2.png)
![5_1D-field-advance_21_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/27cab423-e40b-da28-5e56-1fb24966e85e.png)




## Non-linear Schirodinger方程式

ポテンシャル関数が$V(x)=|\Psi|^2$(引力)のときも有名です．
$$
    i\partial_t \Psi = -\left(\frac{1}{2}\partial_{xx}+ |\Psi|^2\right)\Psi
$$



```python
# parameters
nx = 200
Δx = 10./(nx-1)
# init
x  = np.linspace(-5.0,5.0,nx) 
Ψ  = 2.*np.exp(-x**2) + 0.j
init   = Ψ
# time
t_span = [0.,np.pi]
t_eval = np.linspace(*t_span,400) # time for sampling
# ODEs
def ODEs(t,Ψ,Δx):
    dΨ = -1.j*(- 0.5*np.diff(Ψ,2)/(Δx**2) - (np.abs(Ψ)**2*Ψ)[1:-1])
    return np.hstack([0.+0.j,dΨ,0.+0.j])
# solve
sol = solve_ivp(ODEs,t_span,init,method="RK45",t_eval=t_eval,args=(Δx,),rtol=1e-8)
```


```python
fig,ax = plt.subplots()
ax.imshow(sol.y[:].real,cmap='jet')
fig,ax = plt.subplots()
ax.imshow(sol.y[:].imag,cmap='jet')
```



![5_1D-field-advance_25_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/355bfee8-1ae8-7e1b-0482-59fc8fc23c99.png)
![5_1D-field-advance_25_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/0711855c-d569-9fbb-301a-d7086718c6c3.png)


# 2次元場PDE (基礎編)


```python
import numpy as np
from scipy.integrate import odeint
from matplotlib import pyplot as plt
from scipy.signal import convolve2d
from scipy.integrate import solve_ivp
from mpl_toolkits.mplot3d import Axes3D
```

## 拡散方程式

温度場の二次元拡散方程式を計算してみましょう．
$$
    \partial_t \theta = k(\partial_{xx}+\partial_{yy}) \theta
$$

ラプラシアンを計算する時scipy.convolve2dが便利です．"fill"オプション指定で0境界，"wrap"で周期境界，"symm"で勾配なし境界条件が作れます


```python
# parameter
N = Nx = Ny = 25
Δ = Δx = Δy = 1./(N-1)
k = 0.01
D = (k/Δ**2)
A = np.array([[0.0,   D,0.0],
              [D  ,-4*D,  D],
              [0.0,   D,0.0],])
# init
x = np.linspace(0.0,1.0,N)
θ = np.zeros((N,N))
θ[0,N//2] = 50

t_span=[0.0,1.0]
t_eval=np.linspace(*t_span,3)

# ODEs
def ODEs(t,θ):
    θ = θ.reshape(N,N)
    dθ = convolve2d(θ,A,'same','wrap')
    return dθ.flatten()
#
sol = solve_ivp(ODEs,t_span,θ.reshape(-1),'RK45',t_eval)
```


```python
x = np.linspace(0.0,1.0,N)
x,y = np.meshgrid(x,x)
for θ in sol.y.T:
    θ = θ.reshape(N,N)
    fig,ax = plt.subplots(subplot_kw=dict(projection='3d',zlim=(0.0,1.0)))
    ax.scatter(x,y,θ)
    plt.show()
```


![6_2D-field_5_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/3391c988-8cde-b170-fb53-87e99dcd8c79.png)
![6_2D-field_5_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/fe4e693d-c495-ab36-e14d-e1ff14a09239.png)
![6_2D-field_5_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/2702ba5c-1e5d-f34a-8523-0ca496f1758d.png)


## Gray-Scott モデル

反応拡散方程式のインスタンスの一つです．
$$
    \partial_t u = D_u (\partial_{xx}+\partial_{yy})u + f(u,v) \\
    \partial_t v = D_v (\partial_{xx}+\partial_{yy})u + g(u,v) 
$$
のうち，関数$f,g$を
$$
    f(u,v) = - uv^2 + F(1-u) \\
    g(u,v) = + uv^2 - v(F+k) 
$$
と与えたもの．

説明は例えば，こちらの記事をどうぞ．
https://qiita.com/kaityo256/items/3c07252ab63591256835
コード中のパラメータも参考にさせていただきました．


```python
# parameter
N = Nx = Ny = 40
Δ = Δx = Δy = 1.0
Du = (0.1 /Δ**2)
Dv = (0.05/Δ**2)
F = 0.04
k = 0.06075
A = np.array([[0.0 ,   Du, 0.0],
              [Du  ,-4*Du,  Du],
              [0.0 ,   Du, 0.0],])
B = np.array([[0.0 ,   Dv, 0.0],
              [Dv  ,-4*Dv,  Dv],
              [0.0 ,   Dv, 0.0],])

# init
x = np.linspace(0.0,1.0,N)
u = np.zeros((N,N))
v = np.zeros((N,N))
init = ()
h = N//2
u[h-3:h+3, h-3:h+3] = 0.9
v[h-1:h+1, h-1:h+1] = 0.7

t_span=[0.0,3000.0]
t_eval=np.linspace(*t_span,4)

# ODEs
def ODEs(t,U):
    u,v = U[:N*N].reshape(N,N),U[N*N:].reshape(N,N)
    du = convolve2d(u,A,'same','wrap') - u*v**2 + F*(1.0-u)
    dv = convolve2d(v,B,'same','wrap') + u*v**2 - v*(F + k)
    return np.hstack([du.flatten(),dv.flatten()])
#
sol = solve_ivp(ODEs,t_span,np.hstack([u.flatten(),v.flatten()]),'RK45',t_eval,rtol=1e-5)
```

とても面白い模様が見れます. 他の例としてはFitzHugh–Nagumo方程式なども有名みたいです．


```python
for u in sol.y.T[:,:N*N]:
    u = u.reshape(N,N)
    fig,ax = plt.subplots(figsize=(4,4))
    ax.imshow(u,cmap='Blues')
    plt.show()
```


![6_2D-field_10_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/838e0415-1be4-5f2b-e27a-872418bc63b8.png)
![6_2D-field_10_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/b9d78561-270b-c771-af3e-6bf292551f8b.png)
![6_2D-field_10_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/66908589-4499-5568-55ee-b0e040cdc676.png)
![6_2D-field_10_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/f543194c-5d9f-05ff-69cb-65f50723e168.png)




## BZ反応

平衡点周りを振動しつつ，ゆっくりと平衡状態に近づく反応です．
モデルはいくつかあるみたいですが，今回は下の参考文献中のモデルを用いました．

$$
    \epsilon \partial_t x = x(1-x) -fz\frac{x-q}{x+q}+D_x (\partial_{xx}+\partial_{yy})x \\
    \partial_t z = x-z + D_z (\partial_{xx}+\partial_{yy}) z
$$

これは，
https://cattech-lab.com/science-tools/bz-reaction/
を参考にさせていただきました．


```python
# parameter
N = Nx = Ny = 50
Δ = Δx = Δy = 2.0/(N-1)
Dx = (1.0e-5/Δ**2)
Dz = (1.0e-5/Δ**2)
f = 0.95
ϵ = 0.08
q = 0.075
A = np.array([[0.0 ,   Dx, 0.0],
              [Dx  ,-4*Dx,  Dx],
              [0.0 ,   Dx, 0.0],])
B = np.array([[0.0 ,   Dz, 0.0],
              [Dz  ,-4*Dz,  Dz],
              [0.0 ,   Dz, 0.0],])

# init
x = 0.1*np.ones((N,N))
z = 0.1*np.ones((N,N))
z[N//2-6,N//2] *= 1.01
z[N//2-10,N//2+15] *= 1.01
z[N//2+14,N//2-5] *= 1.01

t_span=[0.0,500.0]
t_eval=np.linspace(*t_span,5)

# ODEs
def ODEs(t,X):
    x,z = X[:N*N].reshape(N,N),X[N*N:].reshape(N,N)
    dx = (x*(1.-x) - f*z*(x-q)/(x+q) + convolve2d(x,A,'same','wrap') )/ϵ
    dz = x-z + convolve2d(z,B,'same','wrap') 
    return np.hstack([dx.flatten(),dz.flatten()])
#
sol = solve_ivp(ODEs,t_span,np.hstack([x.flatten(),z.flatten()]),'RK23',t_eval,rtol=1e-8,atol=1e-6)
```


```python
for u in sol.y.T[:,:N*N]:
    u = u.reshape(N,N)
    fig,ax = plt.subplots(figsize=(4,4))
    ax.imshow(u,cmap='jet')
    plt.show()
```


![6_2D-field_14_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/637beb92-f939-d7f2-fc41-d77ebb0cfda3.png)
![6_2D-field_14_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/cce5fcbc-a19f-0b22-0684-e8d563082309.png)
![6_2D-field_14_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/9486af4e-ce59-6957-4540-dc122e7f99f6.png)
![6_2D-field_14_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/1f941fc6-f4f6-33c8-08a4-5d38306bdbdc.png)


# 2次元場PDE (応用編)


```python
import numpy as np
from scipy.integrate import odeint
from matplotlib import pyplot as plt
from scipy.signal import convolve2d
from scipy.integrate import solve_ivp
```

## TDGLのスピノーダル分解

TDGLは時間依存ギンツブルグランダウの略です．オーダーパラメータ$\psi$の保存則が，
$$
    \psi = (\partial_{xx}+\partial_{yy})\mu
$$
であり，$\mu$はGL-Wilsonハミルトニアンの$\psi$微分であって，
$$
    \mu = - \psi + \psi^3 - (\partial_{xx}+\partial_{yy})\psi
$$
定数を除いて書けるとします．　初期条件として与えたノイズから成長するドメインを見てみましょう．


```python
# parameter
N = Nx = Ny = 40
Δ = Δx = Δy = 1.0
D = (1.0/Δ**2)
A = np.array([[0.0,   D,0.0],
              [D  ,-4*D,  D],
              [0.0,   D,0.0],])
# init
Ψ = 2.0*(np.random.rand(N,N)-0.5)*0.1
Ψ -= Ψ.mean()
init = Ψ.flatten()
# time
t_span=[0.0,20.0]
t_eval=np.linspace(*t_span,3)
# ODEs
def ODEs(t,Ψ):
    Ψ = Ψ.reshape(N,N)
    μ = - Ψ + Ψ**3 - convolve2d(Ψ,A,'same','wrap')
    dΨ = convolve2d(μ,A,'same','wrap')
    return dΨ.flatten()
#
sol = solve_ivp(ODEs,t_span,init,'RK23',t_eval)
```


```python
vmax,vmin = sol.y.max(),sol.y.min()
for Ψ in sol.y.T:
    Ψ = Ψ.reshape(N,N)
    fig,ax = plt.subplots(figsize=(4,4))
#     ax.imshow(Ψ,cmap='jet')
    ax.imshow(Ψ,cmap='jet',vmax=vmax,vmin=vmin)
    plt.show()
```



![7_2D-field_advance_5_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/b24743b5-62a5-82ad-2d61-a017096884e7.png)
![7_2D-field_advance_5_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/e042bcb4-7abb-0392-b89c-06f538a34e7f.png)
![7_2D-field_advance_5_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/3dad8951-32bf-f8e4-d19e-c65e0d837d17.png)


## 非圧縮性Navie-Stokes方程式

2次元に適用できる流れ関数渦度法を用いて，Navie-Stokes方程式の数値解の例を見てみましょう．
$$
    \partial_{t}\omega + \partial_y \phi \partial_x\omega_x- \partial_x\phi\partial_y\omega
    =\nu(\partial_{xx}+\partial_{yy})\omega
$$
$$
    (\partial_{xx}+\partial_{yy})\phi=-\omega
$$

参考資料はこちら．
https://index-press.co.jp/books/digest/ceslib2.pdf


```python
# parameter
N = Nx = Ny = 50
Δ = Δx = Δy = 1.0
s = N
C = np.diag(-4.0*np.ones(s*s  )/Δ**2,k= 0) +\
    np.diag(+1.0*np.ones(s*s-1)/Δ**2,k=-1) +\
    np.diag(+1.0*np.ones(s*s-1)/Δ**2,k=+1) +\
    np.diag(+1.0*np.ones(s*s-s)/Δ**2,k=-s) +\
    np.diag(+1.0*np.ones(s*s-s)/Δ**2,k=+s) 
#
Re=1.
# init
ω = np.zeros((N,N))
ω[N//4,N//4]    =1.0 # vor1
ω[3*N//4,3*N//4]=1.0 # vor2
init = ω.flatten()
# time
t_span=[0.0,80.0]
t_eval=np.linspace(*t_span,5)
# ODEs
def ODEs(t,ω):
    ψ = np.linalg.solve(C,-ω).reshape(N,N)
    ω = ω.reshape(N,N)
    dω = np.zeros((N,N))
    dω[1:-1,1:-1] = 0.25/Δ**2*(ψ[2:]-ψ[:-2])[:,1:-1]*(ω[:,2:]-ω[:,:-2])[1:-1,:] + \
        (1.0/Re)*(np.diff(ω,2,axis=0)[:,1:-1]/Δ**2+np.diff(ω,2,axis=1)[1:-1,:]/Δ**2)
    return dω.flatten()
#
sol = solve_ivp(ODEs,t_span,init,'RK23',t_eval)
```

初期状態で与えた渦が合体して一つの大きな渦になっていく


```python
x = np.linspace(0.,N-1,N)
x = 0.5*(x[1:]+x[:-1])
xx,yy = np.meshgrid(x,x)
for ω in sol.y.T:
    ψ = np.linalg.solve(C,-ω).reshape(N,N)
    vx,vy = np.diff(ψ,1,1)/Δ, -np.diff(ψ,1,0)/Δ
    fig,ax = plt.subplots(figsize=(4,4))
    ax.streamplot(xx,yy,0.5*(vx[1:,:]+vx[:-1,:]).T,0.5*(vy[:,1:]+vy[:,:-1]).T,cmap='jet')
    ax.imshow(np.abs(ψ),cmap='jet')
    plt.show()
```


![7_2D-field_advance_10_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/466981d6-25c5-10f6-0178-8450e0e5fb81.png)
![7_2D-field_advance_10_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/87ebaf9e-03e2-b1fa-7705-03b64f047c46.png)
![7_2D-field_advance_10_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/5f72bc85-36ac-31ae-4401-7723986e42f9.png)
![7_2D-field_advance_10_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/64f0a4c2-2f9d-294d-a0b5-e859b0c757e5.png)
![7_2D-field_advance_10_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669376/96fda92f-da0d-0eae-1153-3c2b78b01082.png)



# あとがき
初投稿ですが記事書くのって難しいですね...

git-hub.pagesにも同じ内容のものをおきました．(というかこっちを先に作った)

https://so-miyamoto.github.io/odes/