偏微分方程式の数値計算¶

一番単純な考え方: method of line¶

例えば、独立変数が空間 $x$ と時間 $t$ の時間発展問題を記述する偏微分方程式は一般に

$$ F(u, \partial u/\partial t, \partial u/\partial x, \cdots) = 0 $$

と書けるが、これを大変多くの式が連立する常微分方程式

$$ \frac{\partial u}{\partial t} = f(u, V, W, \cdots) $$

に帰着ないしは近似させて、この連立常微分方程式を数値計算する方法がいわゆる "method of line" である. (線の方法などと直訳されるが、口語っぽすぎて訳としてわかりにくいかも)

これだけだとよくわからないだろうから、もう少し例を交えて解説すると、たとえば、 $ u(x,t) $ という関数を, $x$ 方向だけ $ u_k(t) \cong u(k\Delta x, t) $ と離散近似化して、 $ u(x,t) $ 全体の代わりに(近似で) $ \{ u_0(t), u_1(t), u_2(t), \cdots \}$ を扱うことにするのだ.

そして、例えば $\partial u/\partial x$ を差分近似 $ ( u_{k+1} - u_{k-1}) / 2\Delta x$ などで置き換えるなどの処理を行えば、上の $f$ を作ることが出来る.

そして、できあがった常微分方程式の数値計算には、これまで学んだ Runge-Kutta 法や線形多段解法、その他もろもろ、どれでも好きな方法を使えば良い.

KdV 方程式を試してみよう¶

Koreweg de Vries 方程式、略して KdV 方程式は、非線形な偏微分方程式であるにも関わらず、進行する孤立波を解として持つという、大変興味深い特徴を持つ方程式だ. ここでは、KdV 方程式として, パラメータ $\epsilon$ をもつ次の式

$$ \frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} + \epsilon^2 \frac{\partial^3 u}{\partial x^3} = 0 $$

を考えることにしよう.

method of line を適用する¶

ここでは、空間領域 $\Omega = [0, L]$ に対し、その分割数を $N$ とし、境界条件として周期的境界条件を設定しよう.

よって、$\Delta x = L/N$ として従属変数を $ U(t) = \{ u_0(t), u_1(t), \cdots , u_{N-1}(t)\}$ とし、境界条件は たとえば仮に外側にはみ出す点を仮想的に考えたとして $$ \left\{ \begin{array}{rcl} \displaystyle u_{-2} & := & u_{N-2}, \\ \displaystyle u_{-1} & := & u_{N-1}, \\ \displaystyle u_{N} & := & u_{0}, \\ \displaystyle u_{N+1} & := & u_{1} \\ \end{array} \right. $$ と解釈することになる.

こうしておいて、空間微分をたとえば差分近似で置き換える. 一階微分の近似である一階中心差分ならば

$$ \delta_k^{(1)} u_k := \frac{ u_{k+1} - u_{k-1} }{2\Delta x} $$

だし、二階中心差分ならば

$$ \delta_k^{(2)} u_k := \frac{ u_{k+1} - 2u_k + u_{k-1} }{\Delta x^2} $$

だし、さらに三階などは

$$ \delta_k^{(3)} := \delta_k^{(1)}\delta_k^{(2)} $$

となる. これらを使った場合は、全体は

$$ \frac{d u_k(t)}{dt} = - u_k (t) \delta_k^{(1)} u_k(t) - \epsilon^2 \delta_k^{(3)} u_k(t), k = 0,1,\cdots,N-1 $$

という N元連立常微分方程式で近似できることになる.

早速計算してみよう!¶

では早速計算してみよう.常微分方程式の解法には Runge-Kutta 法を使うことにしよう.

まずは wikipedia https://ja.wikipedia.org/wiki/KdV%E6%96%B9%E7%A8%8B%E5%BC%8F でも引用されている Zabusky and Kruskal の1965年の古い論文 https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.15.240 で使われているパラメータ等にあわせよう.

In [1]:
const ϵ,L = 0.022, 2
Out[1]:
(0.022, 2)

N や他のパラメータはとりあえず次のような感じで.

In [2]:
N = 200
Δx = L/N
Δt = 0.001
Out[2]:
0.001

配列の添字を自由にしたいので、OffsetArrays package を用いる.¶

(インストールしてない人はインストールしておこう)

In [3]:
using OffsetArrays

すると次のような感じで、自由に決めた添字で配列を使える.

In [4]:
u0 = OffsetArray(Float64, -2:N+1);

境界条件を適用するシーンが何回かありそうなので、関数にしておこう.

In [5]:
function pBC!(u)  # 境界条件に従わせる.
    u[-1] = u[N-1]
    u[-2] = u[N-2]
    u[N]  = u[0]
    u[N+1] = u[1]
    return u
end
Out[5]:
pBC! (generic function with 1 method)

一階から三階まで、差分作用素を関数として定義しておこう.

In [6]:
function diff1(u)
    v = copy(u)
    
    for k in 0:N-1
        v[k] = ( u[k+1] - u[k-1] )/(2Δx)
    end
    
    return pBC!(v)
end
Out[6]:
diff1 (generic function with 1 method)
In [7]:
function diff2(u)
    v = copy(u)
    
    for k in 0:N-1
        v[k] = ( u[k+1] -2u[k] + u[k-1] ) /(Δx^2)
    end
    
    return pBC!(v)
end
Out[7]:
diff2 (generic function with 1 method)
In [8]:
diff3(u) = diff1(diff2(u))
Out[8]:
diff3 (generic function with 1 method)

これで、常微分方程式としての右辺を定義することが出来るようになった.

In [9]:
f(u) = - u .* diff1(u) - ϵ^2 * diff3(u)  # .* で、配列の要素ごとの掛け算が出来る.
Out[9]:
f (generic function with 1 method)

そして、Runge-Kutta 法を定義する.

In [10]:
function RK(u)
    f1 = f(u)
    f2 = f(u + Δt/2 * f1)
    f3 = f(u + Δt/2 * f2)
    f4 = f(u + Δt * f3)
    return u + Δt * (f1 + 2*f2 + 2*f3 + f4)/6
end
Out[10]:
RK (generic function with 1 method)

Zabusky and Kruskal https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.15.240 で使われている初期値を設定しよう.

In [11]:
for k in 0:N-1
    u0[k] = cos(π*k*Δx)
end
pBC!(u0)
Out[11]:
OffsetArrays.OffsetArray{Float64,1,Array{Float64,1}} with indices -2:201:
 0.998027
 0.999507
 1.0     
 0.999507
 0.998027
 0.995562
 0.992115
 0.987688
 0.982287
 0.975917
 0.968583
 0.960294
 0.951057
 ⋮       
 0.951057
 0.960294
 0.968583
 0.975917
 0.982287
 0.987688
 0.992115
 0.995562
 0.998027
 0.999507
 1.0     
 0.999507

試しに、初期値をプロットしてみる.

In [12]:
using Plots
gr()
Out[12]:
Plots.GRBackend()
In [13]:
plot(u0[1:N]) # plot は普通の配列しか理解できないので、そこだけ切り取って渡す
Out[13]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 -1.0 -0.5 0.0 0.5 1.0 y1

ふむ、こんな感じか.

さて、これで準備は整ったので、早速、計算してみよう

In [14]:
u = copy(u0)
sq_u = copy(u0[1:N])  # 記録しておくのは、内点の情報だけでいい.通常の配列扱いできるし.

for i in 1:1000
    u = RK(u)
    if i % 200 == 0  # 200ステップおきにデータを格納するとして
        sq_u = hcat(sq_u, u[1:N])
    end
end

試しに結果の数字をちょろっと見てみる.

In [15]:
sq_u
Out[15]:
200×6 Array{Float64,2}:
 0.999507  0.871751  0.696739   0.403807    -0.576518   -0.602904
 0.998027  0.883419  0.712713   0.216225    -0.559204   -0.49782 
 0.995562  0.894689  0.724192   0.0685818   -0.479925   -0.336876
 0.992115  0.905544  0.730027  -0.0236337   -0.332198   -0.110168
 0.987688  0.915965  0.730365  -0.0459073   -0.100761    0.196353
 0.982287  0.925932  0.726934   0.00891994   0.221211    0.577461
 0.975917  0.935423  0.722938   0.150279     0.63133     1.00675 
 0.968583  0.944416  0.722523   0.377671     1.08832     1.41052 
 0.960294  0.952887  0.729886   0.681382     1.50779     1.68633 
 0.951057  0.96081   0.748191   1.02232      1.76394     1.73734 
 0.940881  0.96816   0.77846    1.33304      1.76168     1.54701 
 0.929776  0.974907  0.818688   1.52149      1.49928     1.18419 
 0.917755  0.981019  0.863473   1.51922      1.07818     0.759683
 ⋮                                                       ⋮       
 0.940881  0.706179  0.53463    0.152727     0.740955    1.14066 
 0.951057  0.721498  0.54771    0.162266     1.04428     0.835579
 0.960294  0.736578  0.559752   0.216979     1.25063     0.489555
 0.968583  0.751409  0.570674   0.315473     1.2935      0.160084
 0.975917  0.765979  0.580689   0.453949     1.15864    -0.115537
 0.982287  0.780279  0.590331   0.614304     0.886094   -0.331509
 0.987688  0.794296  0.600393   0.77233      0.551648   -0.487252
 0.992115  0.808019  0.611766   0.891584     0.219991   -0.595461
 0.995562  0.821435  0.625202   0.94187     -0.0644904  -0.660933
 0.998027  0.83453   0.641055   0.902916    -0.287029   -0.694332
 0.999507  0.847291  0.659049   0.782059    -0.442896   -0.695195
 1.0       0.859704  0.67817    0.602996    -0.538929   -0.667305

あまり変なことにはなっていなさそうなので、プロットしてみよう.

In [16]:
# まず念のために初期値を描いてみる
plot(sq_u[:,1])
Out[16]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 -1.0 -0.5 0.0 0.5 1.0 y1
In [17]:
plot(sq_u[:,6]) # 最後の結果を描いてみる
Out[17]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 -0.5 0.0 0.5 1.0 1.5 2.0 y1
In [18]:
plot(sq_u)  # 一応、全部重ねて描いてみる
Out[18]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 -1 0 1 2 y1 y2 y3 y4 y5 y6

何が起きているか?¶

実は、KdV 方程式の解は複数のソリトンの重ね合わせ的なもので書けることがわかっている.

そして、ソリトンはその「高さ」によって移動速度が異なるため(背の高いほうが速い)、時間発展に従って速いものが前に素早く移動し、遅いものはだんだんとりのこされる.いま計算した結果は、だんだん分離するソリトンを見ていることになる.

wikipedia https://ja.wikipedia.org/wiki/KdV%E6%96%B9%E7%A8%8B%E5%BC%8F の右下に、同じ計算を motion gif にしたものが掲載されているので見てみよう.

レポート課題(チャレンジ課題)¶

上の計算結果を、どうにかして自分で動画にしてみよう.

KdV 方程式の保存量をチェックしよう¶

KdV 方程式には、無限個の保存量があることがわかっている.単純な方から挙げてみると、

$$ \left\{ \begin{array}{rcl} \displaystyle I_1 & := & \int_0^L u dx, \\ \displaystyle I_2 & := & \int_0^L u^2 dx, \\ \displaystyle I_3 & := & \int_0^L \left\{ u^3/3 - \epsilon^2 (u_x)^2 \right\}dx, \\ \displaystyle & \vdots & \end{array} \right. $$

という感じだ.

それぞれ、プログラムしてみよう.

In [19]:
I1(u) = sum(u[1:N]) * Δx
Out[19]:
I1 (generic function with 1 method)
In [20]:
I2(u) = sum( u[1:N].^2 ) * Δx
Out[20]:
I2 (generic function with 1 method)
In [21]:
function I3(u)

    r1 = sum( (u.^3)/3 )
    r2 = sum( (u[k+1] - u[k])^2 for k in 1:N-1 )/(Δx^2)
    r2 += (u[1]-u[N])^2/(Δx^2) # 面倒だけど、こいつもいれておかないとね…
    
    return (r1 - ϵ^2 * r2)*Δx
end
Out[21]:
I3 (generic function with 1 method)

早速、それぞれの量の時間発展を計算してみよう.

In [22]:
sq_I1 = [ I1(sq_u[:,k]) for k in 1:6 ]
Out[22]:
6-element Array{Float64,1}:
 -3.10862e-17
 -1.33227e-17
 -8.54872e-17
 -2.19824e-16
 -2.5091e-16 
 -2.81997e-16
In [23]:
sq_I2 = [ I2(sq_u[:,k]) for k in 1:6 ]
Out[23]:
6-element Array{Float64,1}:
 1.0    
 1.00015
 1.00576
 1.02334
 1.02791
 1.02588
In [24]:
sq_I3 = [ I3(sq_u[:,k]) for k in 1:6 ]
Out[24]:
6-element Array{Float64,1}:
 -0.0047765  
 -0.00477863 
 -0.00395926 
 -0.000880186
 -0.00083225 
 -0.0013602  

とりあえず、この3つの保存量はだいたい保存されていることがみてとれる.

複数のソリトンの挙動を追いかけてみよう¶

先にも書いたように、ソリトンは高いほうが速い.

そこで、二つのソリトンを並べた初期値を用意して、速いほうが遅い方を追い越すシチュエーションを計算してみよう. このとき、phase shift とよばれる現象(追い越すほうが前へ、追い越される方が後ろへジャンプする)が発生するとされているので、それを観測してみよう.

1-ソリトン解¶

高さ $H$, 中心 $x_0$ の 1-ソリトン解は次の式で書ける.

$$ u(x) = H sech^2( \sqrt{\frac{H}{12\epsilon^2}} (x-x_0) ) $$

そこで、少し離してこれを二つ配置することで 2-ソリトンを擬似的に初期配置してみよう.

In [25]:
function OneSoliton(height, center, x) # 1-ソリトン解
    d = sqrt(height/12) / ϵ
    return height * sech(d*(x-center))^2 
end
Out[25]:
OneSoliton (generic function with 1 method)
In [26]:
for k in 0:N-1
    u0[k] = OneSoliton(1, 0.5, k*Δx) + OneSoliton(0.5, 1.2, k*Δx)
end
pBC!(u0)
Out[26]:
OffsetArrays.OffsetArray{Float64,1,Array{Float64,1}} with indices -2:201:
 1.035e-6   
 8.59703e-7 
 8.00653e-6 
 1.04091e-5 
 1.35327e-5 
 1.75936e-5 
 2.28731e-5 
 2.97369e-5 
 3.86603e-5 
 5.02614e-5 
 6.53436e-5 
 8.49514e-5 
 0.000110443
 ⋮          
 4.56736e-6 
 3.7938e-6  
 3.15126e-6 
 2.61754e-6 
 2.17421e-6 
 1.80597e-6 
 1.5001e-6  
 1.24603e-6 
 1.035e-6   
 8.59703e-7 
 8.00653e-6 
 1.04091e-5 

試しにプロットして確認しておこう

In [27]:
plot(u0[1:N])
Out[27]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 0.2 0.4 0.6 0.8 1.0 y1

早速計算してみよう

In [28]:
u = copy(u0)
sq_u = copy(u0[1:N])

for i in 1:5000
    u = RK(u)
    if i % 100 == 0
        sq_u = hcat(sq_u, u[1:N])
    end
end

ではプロットしてみよう

In [29]:
plot(sq_u, legend = :no)
Out[29]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 0.00 0.25 0.50 0.75 1.00

ちょっとわかりにくいので上から見よう.

In [30]:
X = Δx:Δx:2        # 真面目に x の範囲を書き…
T = 0:100Δt:5000Δt    # 真面目に t の範囲を書いて、
plot(X, T, sq_u')
Out[30]:
<?xml version="1.0" encoding="utf-8"?> 0.5 1.0 1.5 2.0 0 1 2 3 4 5 0 0.25 0.50 0.75 1.00

下側の辺が初期値で、全体として上へ時間発展している図になる.

二つのソリトンが基本的に一定速度で動いていること、 背の高い、速いソリトンが後ろから追い越すときに、phase shift が起きていること、などがきちんと見て取れる.

この計算では保存量は?¶

In [31]:
sq_I1 = [ I1(sq_u[:,k]) for k in 1:51 ]
Out[31]:
51-element Array{Float64,1}:
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 ⋮       
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
 0.260198
In [32]:
sq_I2 = [ I2(sq_u[:,k]) for k in 1:51 ]
Out[32]:
51-element Array{Float64,1}:
 0.137543
 0.137546
 0.137547
 0.137548
 0.137548
 0.137548
 0.137548
 0.137548
 0.137547
 0.137546
 0.137546
 0.137545
 0.137543
 ⋮       
 0.137389
 0.13742 
 0.137446
 0.137468
 0.137486
 0.137502
 0.137513
 0.13752 
 0.137527
 0.137532
 0.137537
 0.137539
In [33]:
sq_I3 = [ I3(sq_u[:,k]) for k in 1:51 ]
Out[33]:
51-element Array{Float64,1}:
 0.0239467
 0.0239473
 0.0239477
 0.0239479
 0.0239478
 0.0239477
 0.0239479
 0.0239479
 0.0239477
 0.0239473
 0.023947 
 0.0239466
 0.023946 
 ⋮        
 0.0238668
 0.0238831
 0.0238969
 0.0239082
 0.0239175
 0.0239251
 0.0239305
 0.0239345
 0.0239378
 0.0239403
 0.0239425
 0.0239437
In [34]:
plot(sq_I1 .- 0.260198 , marker = :circle) # 変化が小さいので、おおよその値を引いて、そこからのズレをみる
WARNING: No strict ticks found
WARNING: 
Out[34]:
<?xml version="1.0" encoding="utf-8"?> 0 10 20 30 40 50 -0.00000028922753 -0.00000028922753 -0.00000028922753 -0.00000028922753 y1
No strict ticks found
WARNING: No strict ticks found
In [35]:
plot( sq_I2 .- 0.137543, marker = :circle)
Out[35]:
<?xml version="1.0" encoding="utf-8"?> 0 10 20 30 40 50 -0.0003 -0.0002 -0.0001 0.0000 y1
In [36]:
plot( sq_I3 .- 0.0239467, marker = :circle)
Out[36]:
<?xml version="1.0" encoding="utf-8"?> 0 10 20 30 40 50 -0.00015 -0.00010 -0.00005 0.00000 y1

どうやら、二つのソリトンが衝突するあたりで数値誤差が発生している様子だ.

レポート課題¶

KdV 方程式の数値計算を、method of line + 線形多段解法(+ 予測子修正式法) でやってみよう.