単純な常微分方程式を Euler 法「以外」で計算しようとすると、一般にはどんな技術が必要になるだろうか?

Euler 法は簡単で扱いやすいんだけれども、パラメータを少し変えると不安定になってしまっていた. しかも、詳しく示してはいないけれども、一般に「近似精度」もあまり良くはないんだ.

そこで、もう少し「良い」近似解法が考えられないか、検討してみよう.

対象の微分方程式

対象として今回も前回扱ったものと同じ、人口 $u = u(t)$ ($t$: 時間) に対する常微分方程式 $$ \frac{du}{dt} = \gamma\, \left( \sin(u) \right)^{1.2}, \gamma > 0, t \in (0,\infty) $$ を考えよう. ただし、$u$ の初期値 $u(0)$ については $0 < u(0) < 1$ としておく.

まずは、「対称な」スキームを扱ってみよう

時間刻み幅 $\Delta t$ を一定にして、微分を「中心対称な」差分で近似することで近似計算式(スキームと呼ぶ)を作ることを考えよう. 以降、$u_n \cong u(n\Delta t)$ と想定した近似値 $u_n$ を用いて話を進めよう.

もちろん、中心対称なスキームの作り方も無数にあるが、ここではなるべくシンプルな近似をしてみよう. つまり、時間微分項 $du/dt$ を時間 $t = (n+1/2)\Delta t$ で中心(対称な)差分 $(u_{n+1} - u_n)/\Delta t$ で近似し、 他の部分に出てくる $u(t)$ は $(u_{n+1} + u_n)/2$ などで近似するということになる.

今回の対象としている微分方程式にこうしたシンプルな対称な近似を行ってみると、 $$ \frac{u_{n+1} - u_n}{\Delta t} = \gamma\, \left( \sin( \frac{ u_{n+1} + u_n}{2} )\right)^{1.2} $$ となる.

これはつまり、数値計算のプロセス上では $u_n$ は既知で $u_{n+1}$ は未知量であるので、なにをしないといけないかというと、 関数 $f(u, U)$ ($u_{n+1} = u$, $u_n = U$ と表記)を $$ f(u, U) := u - U - \Delta t \gamma\, \left( \sin( \frac{ u + U }{2} )\right)^{1.2} $$ として与えられた $U$ に対して $f(u, U)$ がゼロになるような $u$ を求めるという「求解」を行なわないといけない、ということになる.

あとは Euler 法同様に、初期値は既知で $u_0 = u(0)$ とすることができるので、さらなる近似値を上のスキームを用いて $u_1, u_2, \cdots$ と順番に求めていけば好きなだけ先の近似値を計算できることがわかる.

Newton 法を用いてみよう!

Newton 法については既に学習したが、復習とプログラムへの慣れを兼ねて、丁寧に見ていこう.

まず、$f(u,U)$ の $u$ に関する微分を求めよう. これは簡単で、 $$ \frac{df}{du} = 1 - 0.6 \Delta t \gamma \sin(\frac{u+U}{2})^{0.2} \cos(\frac{u+U}{2}) $$ となる.

そこで、Newton 法部分も含めて以下のようになるだろう.

In [1]:
# 定数の設定
u0   = 0.1
Δt  = 0.1
γ   = 1.0
Out[1]:
1.0
In [3]:
# 関数 f(u,U)
f(u, U) = u - U - Δt * γ * (sin((u+U)/2))^(1.2)
Out[3]:
f (generic function with 1 method)
In [5]:
# 微分 df/du
df(u, U) = 1 - 0.6 * Δt * γ * (sin((u+U)/2))^(0.2) * cos((u+U)/2)
Out[5]:
df (generic function with 1 method)
In [6]:
# Newton 法で、U から u を求める
function newton(old_u)
    u = old_u # とりあえず、初期値を古い値の U と一致させておいて…
    
    for i in 1:100
      if abs( f(u,old_u) ) < 1.0e-6   # 6桁あってればいいね、という場合はこんな感じで判定する.
        break                       # break というのは一番内側のループ(この場合は for )を脱出する… はず.
      end
      u = u - f(u,old_u)/df(u,old_u) # Newton 法で u を改善する.
    end
    
    return u # f が十分にゼロに近くなったら、近似値 u を返す
end
Out[6]:
newton (generic function with 1 method)

これで準備が整ったので、実際に数値計算をして時間発展をさせてみよう.

In [7]:
u_sq = [u0] # 初期値を配列に入れておいて…
u = u0       # 「今」の値は、もちろん最初は初期値そのもの.

for n in 1:100
    u = newton(u) # Newton 法で新しい時間ステップの値が求まって出てくるはず.
    push!(u_sq, u) # その値を配列に追加していく.
end

計算がスムーズに終わっているようなので、早速、グラフにしてみてみよう.

In [9]:
using Plots
gr()
Out[9]:
Plots.GRBackend()
In [10]:
plot(u_sq)
Out[10]:
<?xml version="1.0" encoding="utf-8"?> 20 40 60 80 100 0.5 1.0 1.5 2.0 2.5 3.0 y1

どうやら無事に計算がうまくいっているようだ.めでたしめでたし.

Euler 法は不安定だったけどこの「対称なスキーム」はどうかな?

前回やってみたように、パラメータによっては Euler 法はふっとんでしまい、不安定だった. この数値スキームはどうだろう? 試してみよう.

In [11]:
Δt = 1.2
Out[11]:
1.2
In [12]:
u_sq = [u0] # 初期値を配列に入れておいて…
u = u0       # 「今」の値は、もちろん最初は初期値そのもの.

for n in 1:100
    u = newton(u) # Newton 法で新しい時間ステップの値が求まって出てくるはず.
    push!(u_sq, u) # その値を配列に追加していく.
end
In [13]:
plot(u_sq)
Out[13]:
<?xml version="1.0" encoding="utf-8"?> 20 40 60 80 100 0.5 1.0 1.5 2.0 2.5 3.0 y1

おお、これはきちんと動作するぞ. どうやら、ちょっとばかり手間だけれども、「対称なスキーム」は Euler 法より安心して使えそうだ

Newton 法の代わりにライブラリに頼ってみよう!

復習を兼ねて、以前学習した、NLsolve パッケージを使ってみよう. まずは、使うとき毎回やるルーチン的な手続きは以下の通りだった.

In [14]:
using NLsolve

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

あとは前やったように、以下のようにすれば良いはずだ.

In [16]:
Δt = 0.1 # 時間刻み幅を元に戻しておこう

u_sq = [u0] # 初期値を配列に入れておいて…
u_old = u0  # 「今」の値は、もちろん最初は初期値そのもの.

for n in 1:100
    u = nls(f, u_old, ini = u_old)[1]  # 関数 nls で新しい値をいきなり求める.
    push!(u_sq, u) # その値を配列に追加していく.
    u_old = u # 古い値を保持する変数に新しい値を入れて、次の時間へ
end

ふむ、無事に動いたようだ.グラフにしてみてみよう.

In [17]:
plot(u_sq)
Out[17]:
<?xml version="1.0" encoding="utf-8"?> 20 40 60 80 100 0.5 1.0 1.5 2.0 2.5 3.0 y1

グラフを見ると、上の例とまったく同じようだ.内容的にも問題ないと見て良いだろう.

念のために、ライブラリを使った場合でもパラメータを変えてみよう

先と同じように、$\Delta t = 1.2$ として挙動を見てみよう.

In [22]:
Δt = 1.2 # 時間刻み幅を大きくしてみる

u_sq = [u0] # 初期値を配列に入れておいて…
u_old = u0  # 「今」の値は、もちろん最初は初期値そのもの.

for n in 1:100
    u = nls(f, u_old, ini = u_old)[1]  # 関数 nls で新しい値をいきなり求める.
    println(n, ": ", u, ": ", sin((u+u_old)/2))
    push!(u_sq, u) # その値を配列に追加していく.
    u_old = u # 古い値を保持する変数に新しい値を入れて、次の時間へ
end
1: 0.2445828206408548: 0.17144028204630682
2: 0.7241519610699485: 0.4656486127362301
3: 1.8709232707904873: 0.962896580041833
4: 2.7192167342635747: 0.7489808758544224
5: 2.985520115427732: 0.2852087610786438
6: 3.0725584588957093: 0.1123158743514151
7: 3.1069544379675222: 0.0518129943639024
8: 3.1225677124829754: 0.026828358987976905
9: 3.1304047960356782: 0.015105824781990605
10: 3.1346510039397333: 0.009064629461162748
11: 3.1370946477756876: 0.005719796543410794
12: 3.1385715278210715: 0.003759556934926038
13: 3.139501169967586: 0.0025563019113543084
14: 3.140106810914135: 0.0017886621951831016
15: 3.140513174918811: 0.00128266032161094
16: 3.1407928947061197: 0.000939618639065662
17: 3.140989805063468: 0.0007013036475126663
18: 3.141131196989348: 0.0005321525382685236
19: 3.1412345332852865: 0.0004097884410071393
20: 3.1413112628758575: 0.0003197555037724399
21: 3.1413690567791157: 0.00025249375962353975
22: 3.1414131562201306: 0.0002015470888057806
23: 3.141447206188787: 0.00016247238461971784
24: 3.1414737826971733: 0.00013215914642826653
25: 3.1414947332426224: 0.00010839561968291782
26: 3.1415114008269587: 8.958655488308557e-5
27: 3.1415247739178596: 7.456621731479871e-5
28: 3.1415355805694105: 6.247634611733667e-5
29: 3.1415443858855614: 5.267036228293089e-5
30: 3.1415516094845417: 4.46559047268447e-5
31: 3.1415575733329284: 3.8062181048994965e-5
32: 3.141562526596144: 3.260362525126794e-5
33: 3.141566663653864: 2.8058464785367872e-5
34: 3.141570137293549: 2.4253116084303685e-5
35: 3.1415730684652154: 2.105071040937595e-5
36: 3.1415755535587575: 1.8342577805956342e-5
37: 3.141577669879381: 1.6041870723227465e-5
38: 3.1415794798010395: 1.4078749582692603e-5
39: 3.14158103394284: 1.2396717852940844e-5
40: 3.1415823736216253: 1.0949807560126888e-5
41: 3.1415835327860404: 9.700385960235457e-6
DomainError:
Exponentiation yielding a complex result requires a complex argument.
Replace x^y with (x+0im)^y, Complex(x)^y, or similar.

Stacktrace:
 [1] nan_dom_err at ./math.jl:300 [inlined]
 [2] ^ at ./math.jl:699 [inlined]
 [3] f(::Float64, ::Float64) at ./In[3]:2
 [4] #2 at ./In[14]:5 [inlined]
 [5] (::NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}})(::Array{Float64,1}, ::Array{Float64,1}) at /////////mnt/juliabox/.julia/v0.6/NLsolve/src/utils.jl:107
 [6] f at /////////mnt/juliabox/.julia/v0.6/NLsolve/src/differentiable_functions.jl:36 [inlined]
 [7] finite_difference_jacobian!(::NLsolve.#f#4{NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}}}, ::Array{Float64,1}, ::Array{Float64,1}, ::Array{Float64,2}, ::Symbol) at /////////mnt/juliabox/.julia/v0.6/Calculus/src/finite_difference.jl:183
 [8] trust_region_(::NLsolve.DifferentiableMultivariateFunction{NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}},NLsolve.#g!#5{NLsolve.#fg!#3{Symbol,NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}}}},NLsolve.#fg!#3{Symbol,NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}}}}, ::Array{Float64,1}, ::Float64, ::Float64, ::Int64, ::Bool, ::Bool, ::Bool, ::Float64, ::Bool) at /////////mnt/juliabox/.julia/v0.6/NLsolve/src/trust_region.jl:106
 [9] #nlsolve#12(::Symbol, ::Float64, ::Float64, ::Int64, ::Bool, ::Bool, ::Bool, ::Function, ::Float64, ::Bool, ::Int64, ::Float64, ::NLsolve.#nlsolve, ::NLsolve.DifferentiableMultivariateFunction{NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}},NLsolve.#g!#5{NLsolve.#fg!#3{Symbol,NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}}}},NLsolve.#fg!#3{Symbol,NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}}}}, ::Array{Float64,1}) at /////////mnt/juliabox/.julia/v0.6/NLsolve/src/nlsolve_func_defs.jl:26
 [10] (::NLsolve.#kw##nlsolve)(::Array{Any,1}, ::NLsolve.#nlsolve, ::NLsolve.DifferentiableMultivariateFunction{NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}},NLsolve.#g!#5{NLsolve.#fg!#3{Symbol,NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}}}},NLsolve.#fg!#3{Symbol,NLsolve.#fvec!#27{##2#4{#f,Tuple{Float64}},Array{Float64,1}}}}, ::Array{Float64,1}) at ./<missing>:0
 [11] #nlsolve#14(::Symbol, ::Float64, ::Float64, ::Int64, ::Bool, ::Bool, ::Bool, ::Function, ::Float64, ::Bool, ::Int64, ::Float64, ::Bool, ::NLsolve.#nlsolve, ::##2#4{#f,Tuple{Float64}}, ::Array{Float64,1}) at /////////mnt/juliabox/.julia/v0.6/NLsolve/src/nlsolve_func_defs.jl:80
 [12] #nls#1(::Float64, ::Function, ::Function, ::Float64, ::Vararg{Float64,N} where N) at ./In[14]:5
 [13] (::#kw##nls)(::Array{Any,1}, ::#nls, ::Function, ::Float64, ::Vararg{Float64,N} where N) at ./<missing>:0
 [14] macro expansion at ./In[22]:7 [inlined]
 [15] anonymous at ./<missing>:?
 [16] include_string(::String, ::String) at ./loading.jl:515

Euler 法よりは随分マシだけれども、この場合も途中で計算が停止してしまう. 今回は出力の $u$ は $\pi$ を超えていないのだけれども、$\sin$ が大変ゼロに近く、かなり微妙な状態になっているようだ.