偏微分方程式¶

もう少し、踏み込む手法の一つ.有限差分法(finite difference method).¶

微分を離散近似するシンプルな手法に、差分法と呼ばれるものがある. これは、前回の method of line で右辺を離散近似する際にも使われたもので、微分を、引き算と割り算で近似するものである.

この手法は単純に見えるが、応用は広く、使い方によっては大変強力だ. 今回、この手法についてまずは入門的な内容で様子をみてみよう.

対象は KdV 方程式で¶

前回紹介した KdV 方程式として, パラメータ $\epsilon$ をもつ次の式

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

を考えることにしよう.

差分法を使ってみる¶

差分法は微分を差分、つまり、Taylor 展開で近似するものである. 近似できていれば良いのだ! と考えれば、こんなに自由な方法もないわけで、まあやりたい放題といって良い.

そこで、たとえば今回は、あちこち平均したり差分したりして作った以下の離散式(数値スキーム)、をもとの KdV 方程式の近似式として考えよう. ただし、$u_k^{(n)} \cong u(k\Delta x, n\Delta t)$ として、下の式は簡潔に書くために $u = u_k^{(n)}$, $u^{+} = u_k^{(n+1)}$ と記している.

$$ \frac{ u^{+} - u }{\Delta t} + \left( \frac{u^{+} + u}{2} \right) \delta_k^{(1)} \left( \frac{u^{+} + u}{2} \right) + \epsilon^2 \delta_k^{(3)} \left( \frac{u^{+} + u}{2} \right) = 0 $$

ただし、$n = 0,1,2, \cdots$, $k = 0, 1, \cdots, N-1$ で、離散境界条件は以前と同じように周期的境界条件を離散化した

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

としよう.

早速計算してみよう!¶

では早速計算へ向けて準備を始めよう.

まずは 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]:
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[4]:
pBC! (generic function with 1 method)

型を一回実際に使っておくと、あとで楽なので、初期値を作ってしまおう.まず、

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

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

In [6]:
u0[0:N-1] = [ cos(π*k*Δx) for k in 0:N-1 ]  # 中身は普通に作って、
pBC!(u0)   # 境界条件を満たすようにする
Out[6]:
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 [7]:
function diff1(u)
    v = similar(u)
    v[0:N-1] = [( u[k+1] - u[k-1] )/(2Δx) for k in 0:N-1 ] 
    return pBC!(v)
end
Out[7]:
diff1 (generic function with 1 method)
In [8]:
function diff2(u)
    v = similar(u)
    v[0:N-1] = [ ( u[k+1] -2u[k] + u[k-1] ) /(Δx^2) for k in 0:N-1 ]  
    return pBC!(v)
end
Out[8]:
diff2 (generic function with 1 method)
In [9]:
diff3(u) = diff1(diff2(u))
Out[9]:
diff3 (generic function with 1 method)

数値スキームそのものを関数にしてしまおう¶

ただし、全体に $\Delta t$ をかけておく.式全体のオーダーを $u$ に合わせたいので.

In [10]:
function f(up, u)
    av = (up+u)/2
    return up - u + Δt * ( av .* diff1(av) + ϵ^2 * diff3(av) )
end
Out[10]:
f (generic function with 1 method)

この式がゼロになるような解 up を「探す」わけだが、それには NLsolve package が使える. しかし、NLsolve の求解は通常の配列しか受け付けないので、 通常の、外へはみ出てない配列と offsetarray を変換する関数を NLsolve 用に作っておく.

In [11]:
toArray(u) = u[0:N-1] # こっちは簡単.というか、定義しなくてもいいぐらい.

function toOffset(u)
    ou = similar(u0) # u0 と同じ型だしね.
    ou[0:N-1] = u #  はみ出ていない中身は本質的に同じ.
    pBC!(ou)
    return ou
end
Out[11]:
toOffset (generic function with 1 method)

これを使って、数値スキームを NLsolve で使える形に直しておこう.

In [12]:
function f_solve(up_in, u_in)  # up_in も u_in も添字が 1:N-1 の普通の配列だとしよう.
    up, u = toOffset(up_in), toOffset(u_in)
    return toArray( f(up,u) ) # 出力も普通の配列にして…
end
Out[12]:
f_solve (generic function with 1 method)

これで良い.この f_solve 関数がゼロになるように数値解を探せば良いことになる. さて、では、NLsolve を使う準備をしよう.

In [13]:
using NLsolve

function nls(func, params...; ini = [0.0])
    if typeof(ini) <: Number
        return nlsolve((vin,vout)->vout[1]=func(vin[1],params...), [ini]).zero[1]
    else
        return nlsolve((vin,vout)->vout .= func(vin,params...), ini).zero
    end
end
Out[13]:
nls (generic function with 1 method)

試しに 1ステップだけ解いてみよう.

In [14]:
u1 = nls(f_solve, toArray(u0), ini= toArray(u0) )
Out[14]:
200-element Array{Float64,1}:
 0.999995
 0.9996  
 0.998218
 0.99585 
 0.992499
 0.988167
 0.982858
 0.976578
 0.969332
 0.961127
 0.951972
 0.941874
 0.930843
 ⋮       
 0.928704
 0.939882
 0.950135
 0.959453
 0.967827
 0.975247
 0.981708
 0.987201
 0.991721
 0.995264
 0.997826
 0.999404

図にしてみて、変でないことをたしかめておこう.

In [15]:
using Plots
gr()
Out[15]:
Plots.GRBackend()
In [16]:
plot( u1 )
Out[16]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 -0.75 -0.50 -0.25 0.00 0.25 0.50 0.75 y1

ふむ、こんな感じか.

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

その前に¶

このスキームは非線形方程式を解くので、時間がかかることが想定される. そこで、計算がどれくらい進んでいるのか、リアルタイムで見えるようにしよう.それには、ProgressMeter というパッケージを使えば良い(インストールしていない人は、Pkg.add コマンドでインストールしておこう).

これを使うと、@showpprogress 時間間隔(秒) メッセージ などを for 文の頭につけるだけで、その for 文の実行状況と、残り推定時間を指定した時間間隔毎に更新して教えてくれる.

In [17]:
using ProgressMeter
In [18]:
u = toArray(u0)   # メタ的視点から見ると、offsetarray は見えない格好になった.よって、ここは通常配列で.
sq_u = copy( u )

# 1step だけ、推測値を現在の値にして, 計算 & 更新
u, u_old  = nls( f_solve, u, ini = u ), u

@showprogress for i in 2:1000  # 環境にもよるが、10分ぐらいかかるか? 
                               # 授業時は 2:200 ぐらいにしておいた方が良さ気.  
   u, u_old = nls( f_solve, u, ini = 2*u - u_old ), u  
                # 2step 目から、推測値を「過去と今から計算」して入力.こうすると何割か早い.
   if i % 100 == 0  # 100ステップおきにデータを格納するとして
        sq_u = hcat(sq_u, u)
    end
end
Progress: 100%|█████████████████████████████████████████| Time: 0:02:45

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

In [19]:
sq_u
Out[19]:
200×11 Array{Float64,2}:
 1.0       0.956153  0.859704  0.757938  …  -0.538929   -0.66376    -0.667301
 0.999507  0.964282  0.871752  0.770684     -0.576519   -0.630957   -0.602904
 0.998027  0.971663  0.88342   0.783294     -0.559205   -0.553665   -0.497812
 0.995562  0.978272  0.89469   0.79573      -0.479927   -0.417903   -0.336871
 0.992115  0.984084  0.905545  0.807932     -0.332199   -0.215148   -0.110155
 0.987688  0.989074  0.915966  0.81983   …  -0.100764    0.0718194   0.196364
 0.982287  0.993214  0.925932  0.831366      0.221209    0.44255     0.57748 
 0.975917  0.996478  0.935423  0.842516      0.631325    0.881213    1.00676 
 0.968583  0.998838  0.944416  0.853315      1.08832     1.32207     1.41053 
 0.960294  1.00027   0.952887  0.86386       1.50778     1.66292     1.68633 
 0.951057  1.00073   0.960811  0.8743    …   1.76394     1.78613     1.73734 
 0.940881  1.00021   0.968161  0.884792      1.76168     1.64679     1.54699 
 0.929776  0.998671  0.974907  0.895442      1.49928     1.29714     1.18418 
 ⋮                                       ⋱                           ⋮       
 0.929776  0.808463  0.690631  0.594284      0.412862    1.14712     1.32352 
 0.940881  0.823853  0.70618   0.608637      0.740933    1.32761     1.14066 
 0.951057  0.838747  0.721498  0.622884  …   1.04426     1.32333     0.835582
 0.960294  0.853127  0.736578  0.63702       1.25063     1.13889     0.489555
 0.968583  0.866973  0.751409  0.651037      1.29351     0.829242    0.160083
 0.975917  0.880266  0.76598   0.664923      1.15866     0.477859   -0.11554 
 0.982287  0.892988  0.780279  0.678666      0.886113    0.143983   -0.33151 
 0.987688  0.905118  0.794297  0.692256  …   0.551663   -0.133731   -0.487255
 0.992115  0.916636  0.80802   0.705686      0.220001   -0.349151   -0.59546 
 0.995562  0.92752   0.821436  0.718959     -0.0644842  -0.500781   -0.660936
 0.998027  0.937749  0.834531  0.732083     -0.287025   -0.600866   -0.69433 
 0.999507  0.947301  0.847292  0.745072     -0.442895   -0.652234   -0.695196

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

In [20]:
# まず念のために初期値を描いてみる
plot(sq_u[:,1])
Out[20]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 -1.0 -0.5 0.0 0.5 1.0 y1
In [21]:
plot(sq_u[:,11]) # 最後の結果を描いてみる
Out[21]:
<?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 [22]:
plot(sq_u)  # 一応、全部重ねて描いてみる
Out[22]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 -1 0 1 2 y1 y2 y3 y4 y5 y6 y7 y8 y9 y10 y11

以前同様、ソリトンへの分解が見られる.¶

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

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 [23]:
I1(u) = sum(u) * Δx
Out[23]:
I1 (generic function with 1 method)
In [24]:
I2(u) = sum( u.^2 ) * Δx
Out[24]:
I2 (generic function with 1 method)
In [25]:
function I3(u)
    r1 = sum( (u.^3)/3 )
    r2 = sum( (vcat(diff(u), u[1] - u[end] )).^2 )/(Δx^2)
    return (r1 - ϵ^2 * r2)*Δx
end
Out[25]:
I3 (generic function with 1 method)

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

In [26]:
sq_I1 = [ I1(sq_u[:,k]) for k in 1:11 ]
Out[26]:
11-element Array{Float64,1}:
 -2.44249e-17
 -2.05391e-16
 -6.67244e-16
 -1.56986e-15
 -8.54872e-16
 -7.64944e-16
 -1.06914e-15
  2.07057e-16
  2.22045e-15
  6.03517e-15
  9.34253e-15
In [27]:
sq_I2 = [ I2(sq_u[:,k]) for k in 1:11 ]
Out[27]:
11-element Array{Float64,1}:
 1.0    
 1.00003
 1.00015
 1.00082
 1.00576
 1.01573
 1.02334
 1.02699
 1.02791
 1.0273 
 1.02588
In [28]:
sq_I3 = [ I3(sq_u[:,k]) for k in 1:11 ]
Out[28]:
11-element Array{Float64,1}:
 -0.0047765  
 -0.00477688 
 -0.00477862 
 -0.00475651 
 -0.00395908 
 -0.002007   
 -0.000879984
 -0.000662641
 -0.000831951
 -0.0011007  
 -0.00135988 

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

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

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

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

1-ソリトン解¶

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

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

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

In [29]:
function OneSoliton(height, center, x) # 1-ソリトン解
    d = sqrt(height/12) / ϵ
    return height * sech(d*(x-center))^2 
end
Out[29]:
OneSoliton (generic function with 1 method)
In [30]:
u0[0:N-1] = [ OneSoliton(1, 0.5, k*Δx) + OneSoliton(0.5, 1.2, k*Δx) for k in 0:N-1]
pBC!(u0)
Out[30]:
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 [31]:
plot( toArray(u0) )
Out[31]:
<?xml version="1.0" encoding="utf-8"?> 50 100 150 200 0.2 0.4 0.6 0.8 1.0 y1

早速計算してみよう

In [32]:
Δt = 0.002 # 計算時間がかかるので、時間ステップ間隔を大きくしてしまえ.

u = toArray(u0)   # メタ的視点から見ると、offsetarray は見えない格好になった.よって、ここは通常配列で.
sq_u = copy( u )

# 1step だけ、推測値を現在の値にして, 計算 & 更新
u, u_old  = nls( f_solve, u, ini = u ), u

@showprogress for i in 2:2500  # 環境にもよるが、15分ぐらいかかるか? 
                                                 # 授業時は 2:200 ぐらいにしておいた方が良さ気.
    
   u, u_old = nls( f_solve, u, ini = 2*u - u_old ), u  # 2step 目から、推測値を「過去と今から計算」して入力.
    
   if i % 100 == 0  # 100ステップおきにデータを格納するとして
        sq_u = hcat(sq_u, u)
    end
end
Progress: 100%|█████████████████████████████████████████| Time: 0:06:51

ではプロットしてみよう

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

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

In [34]:
X = Δx:Δx:2        # 真面目に x の範囲を書き…
T = 0:100Δt:2500Δt    # 真面目に t の範囲を書いて、
plot(X, T, sq_u')
Out[34]:
<?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 [35]:
sq_I1 = [ I1(sq_u[:,k]) for k in 1:26 ]
Out[35]:
26-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
 0.260198
In [36]:
sq_I2 = [ I2(sq_u[:,k]) for k in 1:26 ]
Out[36]:
26-element Array{Float64,1}:
 0.137543
 0.137547
 0.137548
 0.137548
 0.137547
 0.137546
 0.137543
 0.137541
 0.137537
 0.13753 
 0.137513
 0.137489
 0.137449
 0.137391
 0.137318
 0.137245
 0.137205
 0.137218
 0.137277
 0.137354
 0.13742 
 0.137468
 0.137502
 0.13752 
 0.137532
 0.137539
In [37]:
sq_I3 = [ I3(sq_u[:,k]) for k in 1:26 ]
Out[37]:
26-element Array{Float64,1}:
 0.0239467
 0.0239477
 0.0239478
 0.0239479
 0.0239477
 0.023947 
 0.023946 
 0.0239448
 0.0239425
 0.0239386
 0.0239307
 0.0239187
 0.0238984
 0.0238681
 0.0238293
 0.0237908
 0.0237688
 0.0237758
 0.0238076
 0.0238482
 0.023883 
 0.0239082
 0.023925 
 0.0239345
 0.0239403
 0.0239437
In [38]:
plot(sq_I1 .- 0.260198 , marker = :circle) # 変化が小さいので、おおよその値を引いて、そこからのズレをみる
Out[38]:
<?xml version="1.0" encoding="utf-8"?> 5 10 15 20 25 -0.00000028922753 -0.00000028922750 -0.00000028922748 -0.00000028922745 -0.00000028922743 y1
In [39]:
plot( sq_I2 .- 0.137543, marker = :circle)
Out[39]:
<?xml version="1.0" encoding="utf-8"?> 5 10 15 20 25 -0.0003 -0.0002 -0.0001 0.0000 y1
In [40]:
plot( sq_I3 .- 0.0239467, marker = :circle)
Out[40]:
<?xml version="1.0" encoding="utf-8"?> 5 10 15 20 25 -0.00015 -0.00010 -0.00005 0.00000 y1

二つのソリトンが衝突するあたりで数値誤差が発生している様子など、先週の method of line + Runge-Kutta 法による結果とよく似ている.

レポート課題¶

KdV 方程式に対して、差分法によって他の数値スキームを作って、実際に計算して保存量などをチェックしてみよう. 特に、「保存量がなるべく保たれるような数値スキーム」を作れないか、工夫してみよう.