ベキ級数演算を利用しており、基本的にアルゴリズムは、
いろいろな手法の優劣を比較するための多数のファイルがある。
ode.hpp
ode-autodif.hpp
ode-maffine.hpp
ode-param.hpp
ode-callback.hpp
など。下請けとして、
psa.hpp
interval.hpp
interval-vector.hpp
autodif.hpp
affine.hpp
make-candidate.hpp
などが使われている。
以下は実験的アルゴリズムのためのファイル。
ode-affine.hpp
ode-affine-wrapper.hpp
ode-qr.hpp
ode-lohner.hpp
ode-qr-lohner.hpp
ode-maffine2.hpp
以下、最も基本的なodelong_maffineの使い方を説明する。 例えば、常微分方程式
dx0/dt = x1
dx1/dt = -x0
を、初期値
x0(0) = 0
x1(0) = 1
としてt=[0,10]で解くには、次のようなファイルを作ればよい (ode-sample1.cc)。
#include <iostream>
#include <kv/ode-maffine.hpp>
namespace ub = boost::numeric::ublas;
typedef kv::interval<double> itv;
struct Func {
template <class T> ub::vector<T> operator() (const ub::vector<T>& x, T t){
ub::vector<T> y(2);
y(0) = x(1);
y(1) = -x(0);
return y;
}
};
int main()
{
ub::vector<itv> x;
itv end;
int r;
std::cout.precision(17);
x.resize(2);
x(0) = 0.;
x(1) = 1.;
end = 10.;
r = kv::odelong_maffine(Func(), x, itv(0.), end);
if (r == 0) {
std::cout << "Cannot calculate solution.\n";
} else if (r == 1) {
std::cout << "Solution calculated until t = " << end << ".\n";
std::cout << x << "\n";
} else {
std::cout << "Solution calculated.\n";
std::cout << x << "\n";
}
}
これを実行すると、
Solution calculated. [2]([-0.54402111088937289,-0.54402111088936644],[-0.83907152907645577,-0.83907152907644888])のようにx0とx1のt=10における値が表示される。 これはsin(10)とcos(10)である。
まず、上の例のFuncのように、解きたい問題の右辺を関数オブジェクトとして 記述する。この問題は自励系なのでtを使っていないが、強制振動系ならば tも使って記述する。
第1引数に解きたい問題の右辺、第2引数に初期値、第3引数に開始時刻、 第4引数に終了時刻を指定する。 計算結果 (終了時の値) は第2引数を上書きし、また計算が途中で終了した 場合はその終了時刻で第4引数を上書きするので、 第2引数及び第4引数は書き込み可能な変数でなければならない。
戻り値(int型)は、次の意味である。
第5引数で動作のカスタマイズが出来る。第5引数のデフォルト値は
ode_param<T>()というode_param型のインスタンスであり、ODE solverの動作を制御するための パラメータをパックした構造体である。 ode-param.hppで定義されている。 例えば、verbose(饒舌)にして計算中の経過を表示させるには、 次のようにすればよい。 (ode-sample2.cc)。
#include <iostream>
#include <kv/ode-maffine.hpp>
namespace ub = boost::numeric::ublas;
typedef kv::interval<double> itv;
struct Func {
template <class T> ub::vector<T> operator() (const ub::vector<T>& x, T t){
ub::vector<T> y(2);
y(0) = x(1);
y(1) = -x(0);
return y;
}
};
int main()
{
ub::vector<itv> x;
itv end;
int r;
std::cout.precision(17);
x.resize(2);
x(0) = 0.;
x(1) = 1.;
end = 10.;
r = kv::odelong_maffine(Func(), x, itv(0.), end, kv::ode_param<double>().set_verbose(1));
}
これを実行すると、
t: [2.0060630097815228,2.0060630097815229] [2]([0.90675762706802143,0.90675762706802277],[-0.42165223318982914,-0.42165223318982825]) t: [3.8601860547497217,3.8601860547497218] [2]([-0.65832653095074523,-0.65832653095074278],[-0.75273247481848471,-0.75273247481848204]) t: [5.6744656548386611,5.6744656548386612] [2]([-0.57181755641836896,-0.57181755641836473],[0.82038081533622209,0.82038081533622587]) t: [7.4500625086106451,7.4500625086106452] [2]([0.91952774507809498,0.91952774507809965],[0.39302509593101991,0.39302509593102525]) t: [9.2020179791437577,9.2020179791437578] [2]([0.22092224750854755,0.22092224750855389],[-0.97529142339906616,-0.97529142339905972]) t: [10,10] [2]([-0.54402111088937289,-0.54402111088936644],[-0.83907152907645577,-0.83907152907644888])のように途中経過が表示される。 カスタマイズ可能なパラメータは、
| パラメータ名 | 意味 | デフォルト値 |
|---|---|---|
| order | order of Taylor expansion | 24 |
| epsilon | used for deciding error tolerance of each step | numeric_limits<T>::epsilon() |
| iteration | maximum number of iterative refinement | 2 |
| restart_max | 精度保証に失敗した時ステップ幅を半分にしてやり直すが、そのやりなおしの最大回数。 | 2 |
| ep_reduce | affine arithmeticのepsilonの削減アルゴリズムの動作を設定。0なら削減アルゴリズムを使わない。正なら、設定された値がそのままepsilonの数の上限値の設定となる。 | 0 |
| ep_reduce_limit | ep_reduce=0なら意味なし。ep_reduce>0のとき、「epsilonの数がep_reduce_limitを超えたらep_reduceまでepsilonを減らす」という動作になる。ep_reduce_limit ≥ ep_reduceである必要がある。 | 0 |
| verbose | if verbose==1 then become verbose | 0 |
がある。複数のパラメータをセットしたいときは、
r = kv::odelong_maffine(Func(), x, itv(0.), end, kv::ode_param<double>().set_verbose(1).set_order(12));
のようにドットを連続して書けばよい。
なお、ode-paramのテンプレートパラメータは、ode_longに渡す値(区間)の 両端に使う型と一致していなければならない。 すなわち、interval<dd>で計算したいなら、ode_param<dd>を 使う必要がある。
odelong_*の名前の関数では、基本的に積分区間を自動分割して計算を行う。 ステップ幅の自動調節の詳細は、 ベキ級数演算について の9章の通り。
例えば、値を「密出力」する(ユーザが指定した時間間隔で結果を表示する) ことができる。
odelong_maffineは更に第6引数を持っており、 そのdefault値はode_callback<T>()である。 ode_callbackはode-callback.hで定義されており、その中身は次のようになっている。
template <class T> struct ode_callback {
virtual bool operator()(const interval<T>& start, const interval<T>& end, const ub::vector< interval<T> >& x_s, const ub::vector< interval<T> >& x_e, const ub::vector< psa< interval<T> > >& result) const {
return true;
}
};
odelong_maffineでは、1ステップ進む毎に、この第6引数に指定された
オブジェクトを
例えば、ode-sample3.ccのようにする。この例では、
namespace kv {
template <class T> struct ode_callback_sample : ode_callback<T> {
virtual bool operator()(const interval<T>& start, const interval<T>& end, const ub::vector< interval<T> >& x_s, const ub::vector< interval<T> >& x_e, const ub::vector< psa< interval<T> > >& result) const {
std::cout << "t: " << end << "\n";
std::cout << "x: " << x_e << "\n";
return true;
}
};
}
のようにode_callbackを継承してode_callback_sampleを作りoperator()を
カスタマイズし、
r = kv::odelong_maffine(Func(), ix, itv(0.), end, kv::ode_param<double>(), kv::ode_callback_sample<double>());
のようにカスタマイズしたインスタンスを渡している。
こうすると、計算中に1ステップ進む毎に定義したoperator()が実行される。
実行結果は次の通り。
t: [2.0060630097815228,2.0060630097815229] x: [2]([0.90675762706802143,0.90675762706802277],[-0.42165223318982914,-0.42165223318982825]) t: [3.8601860547497217,3.8601860547497218] x: [2]([-0.65832653095074523,-0.65832653095074278],[-0.75273247481848471,-0.75273247481848204]) t: [5.6744656548386611,5.6744656548386612] x: [2]([-0.57181755641836896,-0.57181755641836473],[0.82038081533622209,0.82038081533622587]) t: [7.4500625086106451,7.4500625086106452] x: [2]([0.91952774507809498,0.91952774507809965],[0.39302509593101991,0.39302509593102525]) t: [9.2020179791437577,9.2020179791437578] x: [2]([0.22092224750854755,0.22092224750855389],[-0.97529142339906616,-0.97529142339905972]) t: [10,10] x: [2]([-0.54402111088937289,-0.54402111088936644],[-0.83907152907645577,-0.83907152907644888])
test/test-ode-callback.ccにいくつかカスタマイズのサンプルがある。
注意1 (version 0.4.40で変更された機能) version 0.4.39以前は、このcallback関数の戻り値はvoidだったが、 version 0.4.40でにboolに変更された。odelong_*系の関数はこの戻り値を調べ、もしfalseだったら (指定された終了時刻を無視して)そこまでで計算を中止するようになった。その場合の 戻り値は"3"である。これにより、 version 0.4.39以前のversionのkvを利用してcallback機能を使っているプログラムは コンパイル出来なくなってしまった。 直すには、単に継承して作成したoperator()の戻り値をvoidからboolに変え、 関数本体の最後にreturn true;を補えばよい。 なお、test/test-ode-stop.ccに、この機能を利用して計算を途中で中止するサンプル (ロジスティック方程式で個体数がある上限に達したら中止)がある。
注意2 version 0.4.39以前でtest/test-ode-callback.ccの中にサンプルとして提供されていた
#include <iostream>
#include <kv/ode-maffine.hpp>
#include <kv/autodif.hpp>
namespace ub = boost::numeric::ublas;
typedef kv::interval<double> itv;
struct Func {
template <class T> ub::vector<T> operator() (const ub::vector<T>& x, T t){
ub::vector<T> y(2);
y(0) = x(1);
y(1) = -x(0);
return y;
}
};
int main()
{
ub::vector<itv> x;
ub::vector< kv::autodif<itv> > ax;
itv end;
int r;
ub::vector<itv> y;
ub::matrix<itv> dy;
std::cout.precision(17);
x.resize(2);
x(0) = 0.;
x(1) = 1.;
ax = kv::autodif<itv>::init(x);
end = 10.;
r = kv::odelong_maffine(Func(), ax, itv(0.), end);
kv::autodif<itv>::split(ax, y, dy);
std::cout << y << "\n";
std::cout << dy << "\n";
}
実行結果は次の通り。
[2]([-0.54402111088937289,-0.54402111088936644],[-0.83907152907645577,-0.83907152907644888]) [2,2](([-0.83907152907645888,-0.83907152907644544],[-0.54402111088937655,-0.54402111088936322]),([0.54402111088936322,0.54402111088937655],[-0.83907152907645888,-0.83907152907644544]))dyが、初期値(t=0の値)に対してy(t=10の値)を対応させる関数のヤコビ行列 になっている。
odelong_maffineは高精度化のために自動微分versionでなくても内部で このような量を計算しており、自動微分versionはそれを明示的に 取り出せるようにしただけのことである。
注意1 (version 0.4.57で変更された機能) このようにodelong_maffineの初期値として 自動微分型が渡され、なおかつ前節のようにcallback関数が指定された場合は、 callback関数のx_s及びx_eとしてその時刻での解の値に加えて、 初期値に関する微分の情報も与えられるようになった。 例えば問題の次元s=2の場合、x_eは2次元ベクトルではなくs+s×s=6次元 ベクトルとなり、x_e(0)とx_e(1)は従来と同じだが、x_e(2), x_e(3)は x_e(0)を初期値で微分したgradientが、x_e(4)とx_e(5)はx_e(1)を初期値で 微分したgradientが入るようになった。 要はJacobi行列を(C言語式の順序で)flatにしたものが末尾に付加される (例えば ode-sample5.cc )。 過去にcallbackを使ったプログラムとの互換性を保ちつつ、機能を追加するための 苦肉の策である。
test/test-ode.ccやtest/test-ode-autodif.ccに簡単な使い方が書いてある。
最も簡単な1step進む関数odeは、ode.hで定義されており、 例えば次のように使う (ode-sample6.cc)。
#include <iostream>
#include <kv/ode.hpp>
namespace ub = boost::numeric::ublas;
typedef kv::interval<double> itv;
struct Func {
template <class T> ub::vector<T> operator() (const ub::vector<T>& x, T t){
ub::vector<T> y(2);
y(0) = x(1);
y(1) = -x(0);
return y;
}
};
int main()
{
ub::vector<itv> x;
itv end;
int r;
std::cout.precision(17);
x.resize(2);
x(0) = 0.;
x(1) = 1.;
end = 10.;
r = kv::ode(Func(), x, itv(0.), end);
if (r == 0) {
std::cout << "Cannot calculate solution.\n";
} else if (r == 1) {
std::cout << "Solution calculated until t = " << end << ".\n";
std::cout << x << "\n";
} else {
std::cout << "Solution calculated.\n";
std::cout << x << "\n";
}
}
実行すると次のようになる。
Solution calculated until t = [2.0060630097815228,2.0060630097815229]. [2]([0.90675762706802143,0.90675762706802266],[-0.42165223318982914,-0.42165223318982825])引数の意味は第5引数までodelong_*とほぼ同じ。 第1引数は解きたい問題の右辺、第2引数は初期値、第3引数は開始時刻、 第4引数は終了時刻、第5引数はパラメータ。 計算結果 (終了時の値) は第2引数を上書きし、また指定時刻まで計算出来なかった 場合はその終了時刻で第4引数を上書きするので、 第2引数及び第4引数は書き込み可能な変数でなければならない。
戻り値の意味もodelong_*も同じ。
第5引数はパラメータであり、指定の仕方もodelong_*と同様。 使えるパラメータは少し異なり、以下の通り。
| パラメータ名 | 意味 | デフォルト値 |
|---|---|---|
| order | order of Taylor expansion | 24 |
| epsilon | used for deciding error tolerance of each step | numeric_limits<T>::epsilon() |
| iteration | maximum number of iterative refinement | 2 |
| restart_max | 精度保証に失敗した時ステップ幅を半分にしてやり直すが、そのやりなおしの最大回数。 | 2 |
| autostep | trueなら(第4引数に指定された終了時刻を超えない範囲で)step sizeを自動調節し、誤差がepsilon程度になるようにする。falseならepsilonを無視し、指定された終了時刻で精度保証しようとする。 | true |
なお、第6引数に vector< psa < interval <T> > > 型への 変数へのポインタを渡すと、それがNULLで無かった場合には、 開始時刻から終了時刻までの真の解を含む区間多項式を 受け取ることが出来る。defaultはNULLなので、内部で生成された 区間多項式のデータは捨てられる。 以下に、真の解を含む多項式を取得してそれを表示する例を示す。 ただし、表示が長くなるので次数を5次とした (ode-sample7.cc)。
#include <iostream>
#include <kv/ode.hpp>
namespace ub = boost::numeric::ublas;
typedef kv::interval<double> itv;
struct Func {
template <class T> ub::vector<T> operator() (const ub::vector<T>& x, T t){
ub::vector<T> y(2);
y(0) = x(1);
y(1) = -x(0);
return y;
}
};
int main()
{
ub::vector<itv> x;
itv end;
int r, i;
ub::vector< kv::psa<itv> > result_psa;
std::cout.precision(17);
x.resize(2);
x(0) = 0.;
x(1) = 1.;
end = 10.;
r = kv::ode(Func(), x, itv(0.), end, kv::ode_param<double>().set_order(5), &result_psa);
if (r == 0) {
std::cout << "Cannot calculate solution.\n";
} else if (r == 1) {
std::cout << "Solution calculated until t = " << end << ".\n";
std::cout << x << "\n";
} else {
std::cout << "Solution calculated.\n";
std::cout << x << "\n";
}
for (i=0; i<result_psa.size(); i++) {
std::cout << result_psa(i) << "\n";
}
}
実行結果は次の通り。
Solution calculated until t = [0.0021614881244146895,0.0021614881244146896]. [2]([0.0021614864413251936,0.0021614864413251941],[0.99999766398545342,0.99999766398545354]) [[0,0],[1,1],[-0,-0],[-0.16666666666666669,-0.16666666666666665],[0,0],[0.0083333322518444833,0.008333333333333335]] [[1,1],[-0,-0],[-0.5,-0.5],[0,0],[0.041666666666666664,0.041666666666666672],[-3.0020668394648476e-06,-0]]最終項だけ幅の大きい区間となるようなsinとcosのTaylor展開が得られているのが分かる。
なお、関数名は同じodeだが第2引数が自動微分型になっており、初期値に関する 微分を得られる関数が、ode-autodif.hppで定義されている。 使い方は上述のode.hpp内のodeとほぼ同じ。
まずはode_* (1ステップ進む) 系。
| 関数名 | header file | 説明 | |
|---|---|---|---|
| (1) | ode | ode.hpp | 最も普通の1step進む関数。 |
| (2) | ode | ode-autodif.hpp | (1)の第2引数をautodifにしたもの。 |
| (3) | ode_affine | ode-affine.hpp | 第2引数がaffine型。affine型を係数に持つpsaを用いた非常に実験的なもの。高性能だが遅いのであまり実用的ではないと思われる。 | (4) | ode_wrapper | ode-affine-wrapper.hpp | (3)を少し変形したもの。入力されたaffineベクトルをいったん区間ベクトルに直し、改めてaffine化したもので計算する。affineのダミー変数の数が方程式の次元nより多いとき、(3)より性能が落ちるが速い。 | (5) | ode_maffine | ode-maffine.hpp | 第2引数がaffine型。内部では |
(6) | ode_maffine2 | ode-maffine2.hpp | (5)を高速化したもの。初期値に関する微分が出来ないのが欠点。stiffなODEでも比較的大きなstep sizeが取れる。 | (7) | ode_lohner | ode-lohner.hpp | Lohner法を実装したもの。 | (8) | ode_lohner | ode-lohner.hpp | (7)の第2引数をautodifにしたもの。 |
以下はodelong_* (多ステップ進む) 系。
| 関数名 | header file | 説明 | |
|---|---|---|---|
| (9) | odelong | ode.hpp | (1)を単に接続したもの。wrapping effectを防ぐ工夫が皆無なので実用性は無い。 |
| (10) | odelong | ode-autodif.hpp | (2)を単に接続したもの。wrapping effectを防ぐ工夫が皆無なので実用性は無い。 |
| (11) | odelong_affine | ode-affine.hpp | (3)を単に接続したもの。第2引数はaffine型。affine型を使っているのでwrapping effectは起きない。 |
| (12) | odelong_affine | ode-affine.hpp | (11)のwrapperで第2引数がただのintervalになっているもの。 |
| (13) | odelong_wrapper | ode-affine-wrapper.hpp | (4)を単に接続したもの。第2引数はaffine型。 |
| (14) | odelong_wrapper | ode-affine-wrapper.hpp | (13)のwrapperで第2引数がただのintervalになっているもの。 |
| (15) | odelong_maffine | ode-maffine.hpp | (5)を単に接続したもの。第2引数はaffine型。affine型を使っているのでwrapping effectは起きない。 |
| (16) | odelong_maffine | ode-maffine.hpp | (15)のwrapperで第2引数がただのintervalになっているもの。 |
| (17) | odelong_maffine | ode-maffine.hpp | (15)のwrapperで第2引数がautodifになっているもの。 |
| (18) | odelong_maffine2 | ode-maffine2.hpp | (6)を単に接続したもの。第2引数はaffine型。affine型を使っているのでwrapping effectは起きない。 |
| (19) | odelong_maffine2 | ode-maffine2.hpp | (18)のwrapperで第2引数がただのintervalになっているもの。 |
| (20) | odelong_lohner | ode-lohner.hpp | (7)を単に接続したもの。wrapping effectを防ぐ仕組みが皆無なので実用性はない。 |
| (21) | odelong_lohner | ode-lohner.hpp | (8)を単に接続したもの。wrapping effectを防ぐ仕組みが皆無なので実用性はない。 |
| (22) | odelong_qr_lohner | ode-qr-lohner.hpp | (7),(8)で平均値形式を構成し、QR分解を用いた方法で接続したもの。第2引数はただのinterval。 |
| (23) | odelong_qr_lohner | ode-qr-lohner.hpp | (22)と同じだが第2引数がautodif。 |
| (24) | odelong_qr | ode-qr.hpp | (1),(2)で平均値形式を構成し、QR分解を用いた方法で接続したもの。第2引数はただのinterval。 |
| (25) | odelong_qr | ode-qr.hpp | (24)と同じだが第2引数がautodif。 |
たくさんあるが、実際に使用するときにお勧めできるのは上で説明した (1), (16), (17)あたりか。あるいは(16)の高速版としての(19)も勧められる。
これを防ぐために、あまり役に立っていないepsilonを削減して速度低下を 抑制する方法が実装されている。
r = kv::odelong_maffine(Func(), x, itv(0.), end, kv::ode_param<double>().set_ep_reduce(250).set_ep_reduce_limit(300));
のようにパラメータを設定する。
「epsilonの数が300個を超えたら250個まで減らす。」という意味である。
epsilonの削減作業そのものに時間がかかるため、
削減が発動する頻度を減らすためにこのように少し差を付けている。
例えば、次のようなプログラムで計算速度を比較してみる (ode-sample8.cc)。
#include <iostream>
#include <kv/ode-maffine.hpp>
namespace ub = boost::numeric::ublas;
typedef kv::interval<double> itv;
struct Func {
template <class T> ub::vector<T> operator() (const ub::vector<T>& x, T t){
ub::vector<T> y(2);
y(0) = x(1);
y(1) = -x(0);
return y;
}
};
int main()
{
ub::vector<itv> x;
itv end;
int r;
std::cout.precision(17);
x.resize(2);
x(0) = 0.;
x(1) = 1.;
end = 10000.;
// r = kv::odelong_maffine(Func(), x, itv(0.), end);
r = kv::odelong_maffine(Func(), x, itv(0.), end, kv::ode_param<double>().set_ep_reduce(250).set_ep_reduce_limit(300));
std::cout << x << "\n";
}
あるPC (core i7 4770K) で、削減無しで16秒かかったのが削減ありだと
5.1秒となった。その分最終結果 (sin(10000)とcos(10000)) の精度は
わずかに悪くなる。
250と300は適当に選んだもので、最適パラメータは要調査。
しかし、実装のまずさもあって安定性に問題があり、実際にODE_FAST絡みの バグが何度も見つかっている。原理的にODE_FAST=0とODE_FAST=1では 全く同じ計算結果になるはずであり、普通に使っていて挙動不審だと感じたら -DODE_FAST=0でコンパイルして計算結果を比較してみて欲しい。 そしてもし異なる結果になった場合は是非作者まで報告して欲しい。 ODE_FAST=0の方は十分安定してると思う。
続きはまだ書いてない。