Runge-Kutta 法
これまでのような解が振動する場合には、Euler 法では十分な精度の解が得られないことが確認できました。 前回は、Taylor 展開を用いて Euler 法を導出しました。 これを一般化し、より高次の項を使用する方法が Runge-Kutta 法 です。 Runge-Kutta 法の導出や数理的な性質については次回に回しますが、一般的によく使われる 4 次の Runge-Kutta 法では、以下の式によって関数値を更新します。
x(t1+h)=x(t1)+hf1+2f2+2f3+f46f1=f(t1,x(t1))f2=f(t1+h2,x(t1)+h2f1)f3=f(t1+h2,x(t1)+h2f2)f4=f(t1+h,x(t1)+hf3)
この更新式に基づいて前述の連立微分方程式を解いてコンソールに表示するプログラムは以下のようになります。
function setup() {
noLoop();
}
function draw() {
const steps = 100;
const t0 = 0;
const tTarget = 2 * PI;
const h = (tTarget - t0) / steps;
let t1 = t0;
const d = 2;
const x1 = [1, 0];
const x2 = new Array(2);
const tmp = new Array(d);
const f1 = new Array(d);
const f2 = new Array(d);
const f3 = new Array(d);
const f4 = new Array(d);
for (let i = 0; i < steps; ++i) {
const t2 = t1 + h;
dx(t1, x1, f1);
for (let j = 0; j < d; ++j) {
tmp[j] = x1[j] + (h * f1[j]) / 2;
}
dx(t1, tmp, f2);
for (let j = 0; j < d; ++j) {
tmp[j] = x1[j] + (h * f2[j]) / 2;
}
dx(t1, tmp, f3);
for (let j = 0; j < d; ++j) {
tmp[j] = x1[j] + h * f3[j];
}
dx(t1, tmp, f4);
for (let j = 0; j < d; ++j) {
x2[j] = x1[j] + (h * (f1[j] + 2 * f2[j] + 2 * f3[j] + f4[j])) / 6;
}
console.log(x2[0], x2[1]);
t1 = t2;
for (let j = 0; j < d; ++j) {
x1[j] = x2[j];
}
}
}
function dx(t, xt, dxt) {
dxt[0] = xt[1];
dxt[1] = -xt[0];
}
Runge-Kutta 法での更新量を決定するためにf1
、f2
、f3
、f4
、tmp
の 4 つの配列を用意しています。
fi を計算するたびに 1 回 dx
を呼び出しているので、1 回の更新につき合計で 4 回 dx
を呼び出します。。
dx
の実装は変化しないことに注目しましょう。
実行結果は以下のようになります。
0.9980267285137223 -0.06279051136955548
0.992114702509753 -0.1253332172877223
0.982287254051086 -0.18738129033160678
0.9685831675803243 -0.24868985516799763
0.9510565268552302 -0.3090169549641805
0.9297765015048848 -0.36812450628076665
0.9048270740488197 -0.42577923867801076
0.8763067084564548 -0.48175361532742245
0.8443279615548829 -0.5358267309954384
0.8090170388185989 -0.5877851838552253
0.7705132962942676 -0.6374239176859706
0.7289686906262047 -0.6845470311358883
…
さらにその結果をチャートに表示するプログラムと結果は以下の通りです。
ほぼ完全に一致しているので、数値解を表す赤線はほとんど見えません。
Runge-Kutta 法の導出
前回までに引き続き、以下のような常微分方程式を考えます。
ddtx(t)=f(t,x(t))
時刻 t のときの関数値 x(t) がわかっており、t から h だけ進んだ関数値 x(t+h) について知りたいとします。
x(t) の t1 のまわりでの Taylor 展開は以下の通りでした。
x(t)=∞∑m=0(t−t1)mm!x(m)(t1)
これにより、x(t1+h) は以下のようになります。
x(t1+h)=∞∑m=0hmm!x(m)(t1)
右辺の級数を m=s まで求め、残りの項を Rs と表しましょう。
すなわち、
x(t1+h)=s∑m=0hmm!x(m)(t1)+Rs
となります。
さらに、
ddtx(t)=f(t,x(t))
より、
x(t1+h)=x(t1)+s∑m=1hmm!f(m−1)(t1,x(t))+Rs
となります。
Rs を誤差として切り捨てることで、x(t1+h) の近似値を得ることができました。 この近似値は hs までは Taylor 展開と厳密に一致するはずです。 ある近似値 ˜x(t1+h) が x(t1+h) の Taylor 展開の第 s 項目まで一致するとき、˜x(t1+h) の精度の次数は s であると言います。
f(t,x(t)) の s−1 階微分が得られる場合、上記の式は直ちに計算することができます。 しかしながら、必ずしも与えられた微分方程式から高階の導関数を得られるわけではありません。 そのため別の方法で、精度の次数が s となるような近似式を考えなければいけません。
x(t1+h) を以下のような級数で表してみます。
x(t1+h)=x(t1)+hs∑m=1amkm+Rskm=f(t1+cmh,x(t1)+cmhkm−1)2≤m≤sk1=f(t1,x(t1))
ここで a1,⋯,as と c2,⋯,cs は未知の定数係数です。 この級数が Taylor 展開の第s項目までとすれば以下のようになります。
x(t1)+hs∑m=1amkm+Rs=x(t1)+s∑m=1hmm!f(m−1)(t1,x(t1))+Rs
すなわち、以下の条件を満たすように a1,⋯,as と c2,⋯,cs を決めます。
s∑m=1amkm=s∑m=1hm−1m!f(m−1)(t1,x(t1))
実際の係数の求め方は、s に応じて変わります。 精度の次数が s となるように上記の係数を求めた式を s 次の Runge-Kutta 法と呼びます。 前回利用したのは 4 次の Runge-Kutta 法でした。
1 次の Runge-Kutta 法
はじめに精度の次数が 1(s=1)となるような級数について考えてみましょう。
これは次式のようになり、
a1k1=f(t1,x(t1))
また、
k1=f(t1,x(t1))
なので、a1=1 です。
すなわち、
x(t1+h)=x(t1)+hf(t1,x(t1))
となります。
これは、Euler 法の近似式と一致しています。
2 次の Runge-Kutta 法
a1k1+a2k2=f(t1,x(t1))+h2f(1)(t1,x(t1))
k1=f(t,x(t))k2=f(t+c2h,x(t)+c2hki−1)
関数 g(h) を h=0 とした結果を [g(h)]h=0 と表すことにすると、以下の条件式が成り立ちます。
a1[k1]h=0+a2[k2]h=0=f(t1,x(t1))a1[ddhk1]h=0+a2[ddhk2]h=0=12ddxf(t1,x(t1))
ここで、
(a1+a2)f(t1,x(t1))=f(t1,x(t1))
より、
a1+a2=1
となります。
また、
c2a2ddtf(t,x(t))=12ddtf(t,x(t))
より、
a2c2=12
となります。
すなわち、以下の条件のもとで係数を決定する必要があります。
a1+a2=1a2c2=12
これらの係数には自由度があります。 2 次の Runge-Kutta 法は係数の決め方によっていくつかの呼び方があります。
Runge-Kutta の中点法の係数は以下のようになります。
c2=12a1=0a2=1
Heun 法の係数は以下のようになります。
c2=23a1=14a2=34
改良 Euler 法の係数は以下のようになります。
c2=1a1=12a2=12
いずれの場合にも精度の次数が 2 であるための条件が満たされています。
また、3 次以降の Runge-Kutta 法についても同様に導出可能です。