備忘のために。数値解析関連の話はほとんど学んだことがないため、何か間違いがあるかも。
Eular法
以下、例に出そうとしている微分方程式が運動方程式なので、文字の使い方を力学っぽくしている(位置、速度、時間を $x, v, t$ みたいな気持ちで書いている)。
導出(1階)
まず次の常微分方程式がある。
\[ \frac{dx}{dt} = f(t, x) \]
上の式を以下のように近似する。$h$を十分小さくすれば、微分の定義より上の式に近づく。
\[ \begin{aligned} \frac{x(t + h) - x(t)}{h} \simeq f(t, x) \\ \Rightarrow x(t + h) \simeq x(t) + f(t, x)h \end{aligned} \]
これが、$x(t)$の更新式となっている。つまり、ある時刻$t_0$における値$x_0 = x(t_0)$を決めておけば、
\[ \begin{aligned} & t_k := t_{k-1} + h\\ \end{aligned} \]
とおいて、
\[ \begin{aligned} & x(t_1) := x(t_0) + f(t_0, x_0)h \\ & x(t_2) := x(t_1) + f(t_1, x_1)h \\ & x(t_3) := x(t_2) + f(t_2, x_2)h \\ & … \end{aligned} \]
これは無限に続けられるので、プログラムではどこかのタイミングで停止させる。
ということで、Eular法で必要な要素は以下の5つ。
- 解くべき微分方程式
- 初期時刻 $t_0$ と初期値 $x(t_0)$
- 終了時刻 $t_e$
- 刻み幅 $h$
コード
手でも解ける次の微分方程式を、あえてEular法で解く。
\[ \frac{dx}{dt} = -x,\ x(0) = 1; \]
|
|
グラフは以下のようになる。
導出(2階)
まず次の常微分方程式がある。
\[ \begin{aligned} &\frac{dx}{dt} = v \\ &\frac{d^2x}{dt^2} = f(t, v, x) \end{aligned} \]
これは次のように書き換えられる。つまり、$v$と$x$の連立方程式と捉えられる。
\[ \begin{aligned} &\frac{dx}{dt} = v \\ &\frac{dv}{dt} = f(t, v, x) \end{aligned} \]
前と同じ議論によって、以下の形にできる。
\[ \begin{aligned} &x(t + h) \simeq x(t) + vh \\ &v(t + h) \simeq v(t) + f(t, v, x)h \end{aligned} \]
これが更新式。
2階の常微分方程式は、初期条件が2つ必要なことに注意。以下では、$t_0$における位置、速度をそれぞれ $x_0, v_0$ とおいている。
コード
減衰振動の方程式を解いてみる。
\[ \begin{aligned} &\frac{dx}{dt} = v \\ &\frac{dv}{dt} = -\omega^2x - kv\\ &v(0) = 0\\ &x(0) = 10 \end{aligned} \]
今回は、適当に $\omega^2 = 1, k = 0.2$ とする。
|
|
グラフは以下のようになる。
注意
|
|
と
|
|
は意味が違うことに注意。
前者は
\[ \begin{aligned} &v_k := v_{k-1} + f(t_{k-1}, v_{k-1}, x_{k-1})h\\ &x_k := x_{k-1} + v_{k-1}h \end{aligned} \]
という意味だが、後者は、
\[ \begin{aligned} &v_k := v_{k-1} + f(t_{k-1}, v_{k-1}, x_{k-1})h\\ &x_k := x_{k-1} + v_{k}h \end{aligned} \]
という意味になってしまう。つまり、$x_k$を計算するときに$v_k$を使ってしまうことになる。
このような違いが出てしまうのは、コードの実行には順序があることと、変数の再代入ができることが原因。
誤差
Eular法はあまり精度が良くない。例えば、単振動の運動方程式を解くと、誤差が目立つ。
\[ \begin{aligned} &\frac{dx}{dt} = v \\ &\frac{dv}{dt} = -\omega^2x\\ &v(0) = 0\\ &x(0) = 10 \end{aligned} \]
$\omega^2 = 1$ とする。
|
|
単振動なのに、振幅がどんどん大きくなっている。
理由は、以下の式
\[ \begin{aligned} x(t + h) &\simeq x(t) + f(t, v, x)h\\ &= x(t) + \dot{x}(t)h \end{aligned} \]
が、$x(t+h)$ の $h$ まわりのTaylor展開において、2次以上の項を切り落とした式になっているからである。その切り落としの部分が誤差になっている。更新の度に誤差が積もっていくことが、グラフからもよくわかる。
Eular法より少し良い方法
1階の場合
\[ \frac{dx}{dt} = f(t, x) \]
Eular法ではいわゆる「一次近似」を利用していた。それなら二次の項まで使えば、精度が上がりそうである。
\[ x(t+h) \simeq x(t) + \dot{x}(t)h + \frac{1}{2}\ddot{x}(t)h^2 \]
$\dot{x}(t) = f(t, x)$ だが、 $\ddot{x}(t)$ の値が分からない。何かの値で代用する必要がある。
そこで(若干飛躍があるかもしれないが)、以下の式が上の等式と一致するように係数$a, b$を決める。
\[ x(t+h) \simeq x(t) + (a\dot{x}(t) + b\dot{x}(t + h))h \]
これは、一次近似により、
\[ \dot{x}(t + h) \simeq \dot{x}(t) + \ddot{x}(t)h \]
だから、
\[ x(t+h) \simeq x(t) + (a + b) \dot{x}(t) + b\dot{x}(t + h)h^2 \]
係数比較により、$a = b = \frac{1}{2}$ とわかる。
これより、
\[ x(t+h) \simeq x(t) + \frac{\dot{x}(t) + \dot{x}(t + h)}{2}h \]
を元にして更新を行えば、より良い精度が得られそうである。ところが、$\dot{x}(t+h)$ の値もそのままでは計算できない。なぜなら、$\dot{x}(t) = f(t, x)$ は実際には $\dot{x}(t) = f(t, x(t))$ と書くべきで、これを考慮すると$\dot{x}(t+h)$は
\[ \dot{x}(t + h) = f(t + h, x(t + h)) \]
と表せて、結局 $x(t + h)$の値が分からないからである。ここはもう仕方ないので、一次近似により、
\[ \begin{aligned} \dot{x}(t + h) &\simeq f(t + h, x(t) + \dot{x}(t)h)\\ &= f(t + h, x(t) + f(t, x)h) \end{aligned} \]
とする。
以上により、次の手順で$x(t)$から$x(t + h)$を算出すればよい。
\[ \begin{aligned} &k_1 := f(t, x(t))\\ &k_2 := f(t + h, x(t) + k_1h)\\ &x(t + h) := x(t) + \frac{k_1 + k_2}{2}h \end{aligned} \]
2階の場合
\[ \begin{aligned} &\frac{dx}{dt} = v(t)\\ &\frac{dv}{dt} = f(t, v(t), x(t)) \end{aligned} \]
上と同じ議論を行うと、次の式が得られる。
\[ \begin{aligned} &v(t + h) \simeq v(t) + \frac{\dot{v}(t) + \dot{v}(t + h)}{2}h\\ &x(t + h) \simeq x(t) + \frac{\dot{x}(t) + \dot{x}(t + h)}{2}h \end{aligned} \]
$v(t + h)$と$x(t + h)$はそのままでは計算できないので、次のように近似する。
\[ \begin{aligned} \dot{v}(t + h) &= f(t + h, v(t + h), x(t + h))\\ &\simeq f(t + h, v(t) + f(t,v,x)h, x(t) + v(t)h)\\ \dot{x}(t + h) &= v(t + h)\\ &\simeq v(t) + f(t,v,x)h \end{aligned} \]
これにより、次の手順が得られる。
\[ \begin{aligned} k_1 &:= f(t, v(t), x(t))\\ l_1 &:= v(t)\\ k_2 &:= f(t + h, v(t) + k_1h, x(t) + l_1h)\\ l_2 &:= v(t) + k_1h\\ v(t + h) &:= v(t) + \frac{k_1 + k_2}{2}h\\ x(t + h) &:= x(t) + \frac{l_1 + l_2}{2}h \end{aligned} \]
コード
さて、この手順を導出するために、何回か近似を行っている。本当にEular法よりも精度が良くなっているのだろうか。試しにコードを組んでみる。
|
|
次のようなグラフが得られる(青:Eular法、橙:今回の方法)。Eular法に比べて、かなり精度が良くなっている。
これくらい時間を増やすと、誤差が目に見えてくる。
|
|
Runge-Kutta法
もっと良い精度で微分方程式を解くのが、Runge-Kutta法である。
1階の場合。
\[ \frac{dx}{dt} = f(t, x) \]
Runge-Kutta法では、以下のような更新式に従う。導出については詳しく追えていないので、結果だけ示す。恐らく前節と同じような議論を行えば良いのだと思う。Runge-Kutta法にはいくつかの種類がある、以下の形は古典的Runge-Kutta法と呼ばれるもの。
\[ \begin{aligned} x(t + h) &:= x(t) + \frac{k_1 + 2k_2 + 2k_3 + k_4}{6}h\\ k_1 &:= f(t, x(t))\\ k_2 &:= f\left(t + \frac{h}{2}, x(t) + \frac{h}{2}k_1\right)\\ k_3 &:= f\left(t + \frac{h}{2}, x(t) + \frac{h}{2}k_2\right)\\ k_4 &:= f\left(t + h , x(t) + \frac{h}{2}k_3\right) \end{aligned} \]
2階の場合。
\[ \begin{aligned} &\frac{dx}{dt} = v(t)\\ &\frac{dv}{dt} = f(t, v(t), x(t)) \end{aligned} \]
Runge-Kutta法による更新式は以下のようになる。こちらは前節の類推で作っただけなので、合ってるか少し不安。
\[ \begin{aligned} v(t + h) &:= v(t) + \frac{k_1 + 2k_2 + 2k_3 + k_4}{6}h\\ k_1 &:= f(t, v(t), x(t))\\ k_2 &:= f\left(t + \frac{h}{2}, v(t) + \frac{h}{2}k_1, x(t) + \frac{h}{2}l_1\right)\\ k_3 &:= f\left(t + \frac{h}{2}, v(t) + \frac{h}{2}k_2, x(t) + \frac{h}{2}l_2\right)\\ k_4 &:= f\left(t + h , v(t) + hk_3 , x(t) + hl_3\right)\\ \\ x(t + h) &:= x(t) + \frac{l_1 + 2l_2 + 2k_3 + l_4}{6}h\\ l_1 &:= v(t)\\ l_2 &:= v(t) + \frac{h}{2}k_1\\ l_3 &:= v(t) + \frac{h}{2}k_2\\ l_4 &:= v(t) + hk_3 \end{aligned} \]
$k_1$と$l_1$を計算すれば、$k_2$と$l_2$が計算できる。それが計算できれば、$k_3$と$l_3$が計算できる。同様に、$k_4$と$l_4$が計算できる。プログラムでは、この順で計算すれば良い。
コード
単振動の方程式を解いてみる。
\[ \begin{aligned} &\frac{dx}{dt} = v \\ &\frac{dv}{dt} = -x\\ &v(0) = 0\\ &x(0) = 10 \end{aligned} \]
|
|
ちゃんと正弦波を描いている。
前節の方法と比較してみる。
|
|
グラフは次のようになる(青:前節の方法、橙: Runge-Kutta法)。Runge-Kutta法の方が精度が良いことが分かる。
ライブラリを使う(SciPy)
以上、頑張って色々書いたが、自分でコードを一から書くとバグを含む可能性があるので、ライブラリを素直に使った方がよい。幸運にも、SciPyに常微分方程式の関数がある。
scipy.integrate.solve_ivp
を使う。
以下、リファレンスから抜粋。デフォルトだとRunge-Kutta法になるみたい。
|
|
これは、次の微分方程式の解を計算してくれる。t_span
はt
の範囲のことで、タプル(t0, t_end)
のように指定する。刻み幅は適当に決めてくれる。y0
はt = t0
のときの初期値。
\[ \frac{d\boldsymbol{v}}{dt} = f(t, \boldsymbol{v}) \]
コード
減衰振動の微分方程式をベクトルで表記する。
\[ \begin{aligned} \frac{d}{dt} \left( \begin{matrix} x \\ v \end{matrix} \right) &= \left( \begin{matrix} v \\ -\omega^2x -kv \end{matrix} \right)\\ \left(\begin{matrix} x(0) \\ v(0) \end{matrix}\right) &= \left(\begin{matrix} 10 \\ 0 \end{matrix}\right) \end{aligned} \]
ここでは、$\omega^2 = 1$、$k=0.2$とする。
$x$をv[0]
、$v$をv[1]
の気持ちで、次のように書く。solver.t
で時刻の列、solver.y
でそれに対する値の列を取得できる。
|
|
ただ、このコードだと、グラフがギザギザになっている。
もっと滑らかなものが欲しい場合、次のようにする。solve_ivp
の引数にdense_output=True
を追加し、solver.sol
で時刻ごとの値を計算する。
|
|