Upload
others
View
4
Download
0
Embed Size (px)
Citation preview
早稲田大学理工学部
情報関連科目
Cプログラミング
木曜日1限
2010年度後期
目次
1 コンパイル手順の復習 1
1.1 ソースの編集と実行ファイルの作成 . . . . 1
1.2 具体的手順 . . . . . . . . . . . . . . . . . . 2
2 プログラミング補助ツール 3
2.1 分割コンパイルと make . . . . . . . . . . . 3
2.2 ソースコードの管理:diff,patch . . . . . . 4
3 配列とポインタ 5
3.1 C言語の 2次元配列 . . . . . . . . . . . . . 5
3.2 配列が関数の引数に宣言された場合 . . . . 5
3.3 任意の大きさの 2次元配列 . . . . . . . . . 6
3.3.1 転置行列 . . . . . . . . . . . . . . . 7
4 連立方程式の解法と行列 8
4.1 ガウスの消去法 . . . . . . . . . . . . . . . 8
4.1.1 1次元配列化とメモリ確保 . . . . . 9
4.1.2 ピボット選択 . . . . . . . . . . . . 10
4.1.3 可変長配列:C99の新機能 . . . . . 11
4.2 LU 分解法 . . . . . . . . . . . . . . . . . . 12
4.2.1 上三角行列 U と下三角行列 L . . . 12
4.2.2 LU 分解の効能 . . . . . . . . . . . . 14
4.3 LAPACKの利用 . . . . . . . . . . . . . . . 14
4.3.1 連立 1次方程式の解法: DGESV . . 14
5 多項式の評価 17
5.1 ホーナー法 . . . . . . . . . . . . . . . . . . 17
5.2 多項式による関数の近似 . . . . . . . . . . 18
5.2.1 関数のテーラー展開 . . . . . . . . . 18
5.3 コンパイラーの最適化オプション . . . . . 19
5.3.1 取り敢えず - O2 . . . . . . . . . . 19
5.4 誤差 . . . . . . . . . . . . . . . . . . . . . . 20
5.4.1 二次方程式の解の公式 . . . . . . . 20
6 方程式の解:Newton法 21
6.1 数値解とは . . . . . . . . . . . . . . . . . . 21
6.2 2分法 (Bisection) . . . . . . . . . . . . . . . 21
6.3 はさみうち法 (False position) . . . . . . . . 22
6.3.1 修正はさみうち法 . . . . . . . . . . 23
6.4 Newton法 . . . . . . . . . . . . . . . . . . . 24
6.4.1 微分値の数値計算 . . . . . . . . . . 25
6.5 もっと収束の速い方法 . . . . . . . . . . . . 25
6.6 収束の比較 . . . . . . . . . . . . . . . . . . 26
7 常微分方程式の解法 27
7.1 オイラー法 . . . . . . . . . . . . . . . . . . 27
7.2 ホイン法 . . . . . . . . . . . . . . . . . . . 28
7.3 4次のルンゲ・クッタ法 . . . . . . . . . . . 29
7.4 2階常微分方程式 . . . . . . . . . . . . . . 30
7.4.1 3次元への拡張 . . . . . . . . . . . 30
7.4.2 グローバル変数 . . . . . . . . . . . 32
8 gnuplotを利用した計算結果の可視化 34
8.1 データファイルのプロット . . . . . . . . . 34
8.1.1 データファイルの準備 . . . . . . . 34
8.1.2 gnuplotの起動:対話モード . . . . 34
8.1.3 2次元プロット:plot . . . . . . . . 34
8.1.4 複数のプロット . . . . . . . . . . . 35
8.2 スクリプトファイル . . . . . . . . . . . . . 35
8.2.1 3次元プロット:splot . . . . . . . . 36
8.3 パイプ接続:popen() . . . . . . . . . . . . . 37
8.3.1 書き込みパイプのオープン . . . . . 37
8.3.2 gnuplotの標準入力:’-’ . . . . . . . 37
8.3.3 gnuplotによるアニメーション . . . 39
8.3.4 リングバッファ . . . . . . . . . . . 40
9 簡易グラフィックライブラリ EGGX 42
9.1 EGGX/ProCALLのインストール . . . . . . 42
9.1.1 ユーザーインストール . . . . . . . 42
9.1.2 お試し . . . . . . . . . . . . . . . . 42
9.2 基本的な機能 . . . . . . . . . . . . . . . . . 43
9.2.1 ウィンドウのオープン . . . . . . . 43
9.2.2 基本図形と文字列 . . . . . . . . . . 43
9.2.3 描画速度テスト . . . . . . . . . . . 44
9.2.4 画像の表示:putimg24() . . . . . 44
9.3 アニメーション:copylayer() . . . . . . 45
9.3.1 2つのウィンドウ . . . . . . . . . . 46
9.3.2 キー入力 . . . . . . . . . . . . . . . 47
9.3.3 マウス入力 . . . . . . . . . . . . . . 49
10 GUIツールキット:Tcl&Tk 51
10.1 Tkインタプリタ:wish . . . . . . . . . . . 51
10.1.1 まずは終了ボタン . . . . . . . . . . 51
10.1.2 外部コマンドの起動 . . . . . . . . . 52
10.1.3 オプション値の引渡し . . . . . . . 52
11 応用例 54
11.1 二重振子 . . . . . . . . . . . . . . . . . . . 54
11.1.1 運動方程式 . . . . . . . . . . . . . . 54
11.1.2 微小振動の解析解 . . . . . . . . . . 55
A 解析力学 57
A.1 二重振り子の Lagrange運動方程式 . . . . . 57
A.2 二重振り子の Hamilton方程式 . . . . . . . 57
B Runge-Kutta法の一般形:陰的 RK法 58
B.1 陽的 RK法 . . . . . . . . . . . . . . . . . . 58
B.2 2段 4次陰的 RK法 . . . . . . . . . . . . . 58
B.2.1 調和振動子への適用 . . . . . . . . . 59
B.2.2 単振り子への適用 . . . . . . . . . . 60
B.2.3 二重振り子への適用 . . . . . . . . . 60
C 多変数の Newton法 60
C.1 2変数の場合 . . . . . . . . . . . . . . . . . 61
Cプログラミング 1 コンパイル手順の復習
1 コンパイル手順の復習
1.1 ソースの編集と実行ファイルの作成
C言語ではソースファイルをエディターで書いて,コン
パイラー(ここではGNUの gccを用います)でコンパイル
を行い,実行ファイルを作ります.具体的には,foo.cと
いう名前のソースファイルから barという名前の実行ファ
イルを作成するのには,以下のようにします.� �
$ gcc foo.c -o bar -lm��
��
� �
-o のあとに実行ファイル名を指定します.そうしないと
a.outという名前の実行ファイルが作成されます. -lmは
数学関数ライブラリを利用する場合に必要な(リンク)
オプションです.この他にも,オプション -Wall および
-Wstrict-prototypesを付けると,プログラムが正しく
書かれているかどうかを厳しくかつ細かく警告してくれる
ので,C言語の理解に役立ちます.
なお C言語のソースファイルの拡張子は一般に小文字の
.c としてください.後々学習するかもしれませんが,大
文字の.Cや .cc,.cppがあるソースは,c++のものと自
動的に判断されるのが普通です.
�Commandを逐次実行して確かめながらプログラムを作成する言語は,
インタープリタと呼ばれ(代表は BASICです),初心者に向いています.
しかし,実行速度が遅いことや大規模プログラミングに向いていないな
どの理由で,敬遠されがちです.
source object
executecompile link
source object
source
running
interpret
execute
running
command
File Process
図 1 コンパイル言語(上)とインタプリタ言語(下)の
実行までの概要
例題 1 コンソール出力 『Hello Wold! の表示』は非常に
有名なプログラムです.次の手順でプログラムを作成・実
行しなさい.
( i ) リスト 1のソースをgeditで編集して,ex02.cという
名前で保存.
( ii ) gccでコンパイルして,helloという名前の実行ファイ
ルを作成.
(iii) コマンドラインから ./helloと入力して実行.
リスト 1 ex02.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4
5 int
6 main (int argc, char **argv)
7 {
8 printf ("Hello World!\n");
9 return (0);
10 }
なお,リストの各行頭の数字とコロンは,説明のために紙
面上で付けたものです.実際には入力しないように.
C言語のソースには関数(と呼ばれる機能を発揮する上
での命令文の集まり)の定義と実行手順が記述されます.
最初の 3 行は,いろいろな標準関数が使えるように定義
ファイルを読み込むよう指示するもので “定番”と考えて
ください.実行時には,main関数が最初に動きます,そ
の中から他の関数が次々に呼び出 (call)されます.ところ
で関数は次のような構文に従って定義されます.
関数名 (引数のリスト)
{...
定義
...
}
main も関数ですから 5,6,9,10 行目が型通りの記述で,定
義内容は 8行目だけです.
さて,その内容ですが,書式付き出力 printf("...")
を用いて標準出力(一般的にはディスプレイ) にデータ
を表示しています.基本的には,二重引用符(ダブルクォ
テーション)’’ で挟まれた部分の文字列がそのまま出力さ
れます.しかし,画面制御文字などはもともと対応する可
視文字がないので(そういう意味で不可視文字と呼ばれま
す)頻度の高いものについては,\で始まる記号で表現し
ます.例題中の \nがその例で改行(すなわち,左端にカー
ソルが復帰して行が送られる)を意味します.その他にタ
ブを意味する \tもカラム(column)をそろえる場合によ
く使います.なお,可視文字は \020(8進数)や\x0d(16
進数)のようにコードで表現することもできますが,可読
性に劣るので普通使いません.これらは C言語の printf関
数に限らず,文字に関する慣用的な規約です.もう一つは,
%で始まるもので,変換子と呼ばれ,そこに数値や文字列
が置き換わって表示されます.
第 8,9行目がセミコロン “;”で終了しています.これ
は大変重要で,命令文の終わりを示しています.
目次へ1
Cプログラミング 1 コンパイル手順の復習
1.2 具体的手順
Geditによるファイルの編集と X上の端末エミュレータ
(gnome-term や kterm,rxvt etc. の総称.以降 *term と略
します)上での gccの実行を具体的に説明しましょう.
( i ) *term上で� �
$ gedit & ⇐ & を必ずつけてください� �
とキー入力します.すると図のように geditが画面に
現れます.
図 2 Geditの起動画面
( ii ) 編集の対象となるファイルを選択します.そのために
ツールバーにある「開く」をクリックしてください,図
のようにファイルの選択画面が現れます.新しいファ
イルを編集する場合は,ツールバーにある「新規」を
クリックしてください(新規の場合には保存時に名前
を付ける必要があります).
図 3 Geditのファイル選択画面
(iii) リスト 1と同じ内容 (行頭の番号は入力しないこと)を
作成します.
(iv) ファイルを保存します.ツールバーの「保存」をクリッ
クしてください.
( v ) Geditを起動した *termに� �
samba02:˜$� �
のようなプロンプトが最下行に表示されていれば,コ
マンドを受け付ける状態にありますから,C言語のソー
スのコンパイル命令をキー入力します.以下,シェル
のプロンプトを単に $と略記します.� �
$ gcc ex02.c -o hello -lm��
��
� �
エラーメッセージが表示されなければ成功です.
�Unixは寡黙な OSと呼ばれます.何かの間違いがあった場合に
はエラーがあったと知らせてくれますが,うまくいっている場合に
はわざわざその旨を知らせてくれないのが普通だからです.
(vi) 実行ファイルが出来ているか ls (list)コマンドで確か
めましょう.� �
$ ls hello��
��
hello� �
この場合 hello があれば,単にその名前が表示され
ます.
(vii) hello は実行ファイルです.実行ファイルを実行させ
るには名前をキー入力します.ただし,ファイルの所
在をはっきり示すために,カレントディレクトリを表
す “./”を先頭につけてください.すなわち,� �
$ ./hello��
��
� �
と *term上でキー入力しなければなりません.
�通常 Unix では安全(secure)であることが保証されているディレク
トリにある実行ファイルしか起動しないように制限がかけられています.
これは PATHという環境変数で設定するのが普通です.頻繁に使うプロ
グラムと同名の危険なプログラムが含まれていても,PATHになければ
実行されません.自分で管理していないディレクトリであってもそこに
移ってしまえば,カレントとなるわけですから,カレントディレクトリ
“./”を PATHに含めるのは非常に危険です.カレントにあるファイルは,
(安全であるを)確認して実行させる必要があります.
目次へ2
Cプログラミング 2 プログラミング補助ツール
2 プログラミング補助ツール
2.1 分割コンパイルとmake
規模の大きなプログラムを作成する場合には,ソースも
機能毎に分割してそれぞれのオブジェクトファイルを作成
しておき,あとで一緒にして実行ファイルあるいはライブ
ラリを形成するという方法がとられます.
例えば,あるプロジェクトで project.c(main関数を含む),
sub1.c, sub2.c, 等にソースを分割して管理する場合,以下
のようにして実行ファイルを形成します.� �
$ gcc -c sub1.c��
��
$ gcc -c sub2.c��
��
$ gcc -o project project.c sub1.o sub2.o� �
もし,sub1.cなどに変更があった場合には,以下のように
作り直す必要があります.� �
$ gcc -c sub1.c��
��
$ gcc -o project project.c sub1.o sub2.o��
��
� �
ソースファイルが少ない場合には,依存関係も単純で覚え
ていられますが,分割したソースの数が多い場合は依存関
係をどこかに記しておかないといけません.それが嫌だか
らといって,一つのソースで頑張ろうなどと思わないでく
ださい.長いソースは,編集がとても大変ですし,どこを
直したかも判らなくなりがちです.そんなとき,コンパイ
ルのためのツール make が役立ちます.make(ここでは
GNU make)はコンパイルすべきオブジェクトの依存関係
を記述したファイル GNUmakefile,(あるいは makefile,
Makefile)を元にして,自動的に必要な部分を再コンパ
イルしてくれます.詳細は皆さんの学習に任せることにし
て,最も基本的なな記述ルールを以下に示します.
ターゲット : ターゲットが依存するファイル郡� �TAB� � ターゲットのコンパイル手順 (Command行)
ただし,Command行は,タブ文字で始めます(タブ文字
は普通のエディタでは見えないので付け忘れてしまい,最
初の頃は必ずといっていい程失敗します).
前述の projectの場合にはmakefile(またはGNUmakefile,
Makefile)の内容は以下のようななります.� �
project : project.c sub1.o sub2.o� �TAB� � gcc -o project project.c sub1.o sub2.o
sub1.o : sub1.c� �TAB� � gcc -c sub1.c
sub2.o : sub2.c� �TAB� � gcc -c sub2.c
� �
この makefileがあるディレクトリで単に,� �
$ make��
��
� �
と実行するだけです.何も指定しないと,最初のターゲッ
トしか処理対象としません.途中に記述しているターゲッ
トを処理するには,明示的に
make ターゲット名
とします.大規模なプログラムでは,allという名前のター
ゲットが設けてあり,
make all
により一切合財まとめて作成できるようになっている場合
が多いです.
+参考ウェブサイト:make
• GNU makeの日本語 man
• GNU Make Manual
• GNU make Manualの日本語訳
例題 2 はじめての make ex02.c が dummy1.c,
dummy2.c に依存関係があるとして makefile を記述し,
makeを用いてコンパイルしなさい.
[解] 依存関係を確認するために,中身のない関数を含む
ソースを作成する必要があります.
リスト 2 dummy1.c
1 void sub1(void)
2 {
3 ; /* nothing to do */
4 }
リスト 3 dummy2.c
1 void sub2(void)
2 {
3 ; /* nothing to do */
4 }
以下の makefileを作成して,makeしてみてください.
リスト 4 makefile
1 hello : ex02.c dummy1.o dummy2.o
2 gcc -o hello ex02.c dummy1.o dummy2.o
3 dummy1.o : dummy1.c
4 gcc -c dummy1.c
5 dummy2.o : dummy2.c
6 gcc -c dummy2.c
� �TAB� �� �TAB� �� �TAB� �
helloを作成して以降 ex02.cなどを編集してない場合に
は,「更新済み」というメッセージを出力をして終了します.
処理すべきかどうかは,ファイルの更新日時を比較して判
断します.すなわち,ex02.cの更新日時が helloよりも新
しい場合には内容に変更があったとみなして再コンパイル
をするのです.Unixにはファイルの更新日時を変更するコ
マンド touchがあります.オプション無しで起動すると現
在の日時に変更しますから,� �
$ touch ex02.c��
��
� �
とした後に makeしてみてください.今度は再コンパイル
しますね.dummy1.c, dummy2.cにも touchを作用させた
後 makeして確かめてみましょう.
目次へ3
Cプログラミング 2 プログラミング補助ツール
2.2 ソースコードの管理:diff,patch
プログラムを改良することは即ちソースを修正するとい
うことです.修正したつもりで,新しいバグを混入させて
しまう可能性もあります.どこをどのように修正したか,
修正前後のソースの違いを記録に残して管理したくなりま
す.Unixには,2つのソースの差分を抽出するツール diff
と,差分をあてがってソースを書き換える(修正の前後ど
ちらの方向とも)ツール patchがあります.さらに,現在
ではネットワーク対応の(ソースコード)バージョン管理
システム CVSや Subversionなどが利用されています.
”man diff”してみると判りますが,文法は簡単で以下の
ように 2つのソースを指定するだけです.
diff [options] from-file to-file
オプションに,-uを指定して,出力を拡張子.diffを持つ
パッチファイルに保存するには,入出力リダイレクトを利
用して� �
diff -u foo.c foo.c.new > foo.diff��
��
� �
とします.このパッチがあれば,patchコマンドを以下の
ように用いて,修正前後のソースコードが得られます.� �
patch < foo.diff��
��
� �
内容が新しいものに patch をあてようとすると,”Re-
versed...” という警告が出て,間違って古い内容に書き換
えてしまうことを防止してくれます.
+参考ウェブサイト:バージョン管理システム
• Ximbiot - CVS Wiki
• バージョン管理システム CVSを使う
• Subversion公式サイト
例題 3 ex02.cに新しい内容を付け加えた新しいソース
ex02.c.newを作成して,差分を抽出してパッチファイル
ex02.diffとして保存しなさい.このパッチファイルを用
いて ex02.c の内容が修正前後どちらにも変更できること
を確認しなさい.
[解]
リスト 5 ex02.c.new
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <float.h>
4
5 int
6 main (int argc, char **argv)
7 {
8 printf ("Hello World!\n");
9 printf ("DBL_EPSILON = %g\n", DBL_EPSILON);
10
11 return (0);
12 }
実行例を以下に示します.最初に diffでパッチファイル
ex02.diffを作成し,内容を表示させます.“+”で始まって
いる行は追加されたことを意味し,逆に “-”で始まってい
る行は削除されたことを意味します.続いて patchをあて
た後に ex02.cの内容を lessで確認しています.2回目には,
Reversed...の警告を無視して続行し(yをキー入力)修
正前の内容に戻ったことが確かめられます.
$ diff -u ex02.c ex02.c.new > ex02.diff
$ less ex02.diff
--- ex02.c 2008-09-24 20:31:01.000000000 +0900
+++ ex02.c.new 2008-09-24 20:30:42.000000000 +0900
@@ -1,11 +1,13 @@
#include <stdio.h>
#include <stdlib.h>
-#include <math.h>
+#include <float.h>
int
main (int argc, char **argv)
{
printf ("Hello World!\n");
+ printf ("DBL_EPSILON = %g\n", DBL_EPSILON);
+
return (0);
}
$ patch < ex02.diff
patching file ex02.c
$ less ex02.c
#include <stdio.h>
#include <stdlib.h>
#include <float.h>
int
main (int argc, char **argv)
{
printf ("Hello World!\n");
printf ("DBL_EPSILON = %g\n", DBL_EPSILON);
return (0);
}
$ patch < ex02.diff
patching file ex02.c
Reversed (or previously applied) patch detected! Assume -R? [n] y
$ less ex02.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int
main (int argc, char **argv)
{
printf ("Hello World!\n");
return (0);
}
目次へ4
Cプログラミング 3 配列とポインタ
3 配列とポインタ
3.1 C言語の 2次元配列
行列計算には配列が用いられます.しかも 2次元配列と
いうのが自然な発想です.しかし,実のところC言語には
「連続した領域に要素が並んでいる」という意味の配列は
1次元のものだけが保証されているにすぎません.2次元
配列は,1次元配列の配列で実現されています.もちろん,
100 × 1000次元の配列を期待して
double A[100][1000];
と宣言すれば,要素数 1000 の 1 次元配列(A[i]
i=0,1,..)が個々にですが,順番に領域確保されるの
で,全体として連続した領域に 2 次元配列が確保される
のです.
もちろん,1次元配列をバラバラに確保して,そのポイ
ンタを集めて 2次元配列を形成することも可能ですが,そ
れは却って面倒です.したがって,できる限り 2次元配列
を宣言して使うことを推奨します.この辺りの事情をまず
確かめてみましょう.
�ここで A が 2 次元配列を,添字 i, j を用いて A[i][j] のような表現
で読み書きできるものと考えてみましょう.一般に,a[i] は,*(a+i)
の別表現ですから,A[i][j] は,*(A[i]+j)と表されます.このポイン
タ A[i] が,明示的な 2 次元配列宣言を行うとコンパイル時に決定され
ます(定数ポインタとして).また,ポインタ変数の配列を宣言し,後で
そこに 1 次元配列の先頭アドレス値を代入することも可能です.
例題 4 無理やり 1次元配列を寄せ集めて,2次元配列を
形成してみましょう.
[解] プログラムソース例を以下に示します.
リスト 6 1dto2darray.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main()
5 {
6 int i,j;
7 double *a[3];
8 double a2[2]={4,5}, c[1024],
9 a1[2]={2,3}, a0[2]={0,1};
10
11 for (i = 0; i < 3; i++) {
12 printf("a[%d] = %12p (&%p)\n",i,a[i],&a[i]);
13 }
14 a[0] = a0;
15 a[1] = a1;
16 a[2] = a2;
17 for (i = 0; i < 3; i++) {
18 printf("a[%d] = %12p (&%p)\n",i,a[i],&a[i]);
19 }
20
21 for (i = 0; i < 3; i++) {
22 for (j = 0; j < 2; j++) {
23 printf("a[%d][%d]=%f (&%p)\n",i,j,a[i][j],
24 &a[i][j]);
25 }
26 }
27
28 return (0);
29 }
要素数 3のポインタ変数配列*a[3]に要素数 2の 1次元
配列の定数ポインタ値を代入して,2次元配列を実現して
います.
以下に実行例を示します.ポインタ配列自身が格納さ
れたアドレスは,&a[0] で参照できます.この例では,
0xbff7841cで,&a[1],&a[2]はそこからポインタのサイ
ズ 4ずつ進んでいます.こららのポインタ変数が指し示す 1
次元配列の先頭アドレス(定数ポインタ)は,xbff763e8,
xbff763f8, xbff78408となっています.1次元配列を宣
言する際に,わざと c[1024]を間に挟んだせいで,a[2]が
指す値だけが大きく離れています.すなわち,バラバラに
配置されているのです.それでも,aを通常の 2次元配列
としてアクセスできています.
$ ./1dto2darray
a[0] = 0x80482b8 (&0xbff7841c)
a[1] = 0xb801aff4 (&0xbff78420)
a[2] = 0x80496e0 (&0xbff78424)
a[0] = 0xbff763e8 (&0xbff7841c)
a[1] = 0xbff763f8 (&0xbff78420)
a[2] = 0xbff78408 (&0xbff78424)
a[0][0]=0.000000 (&0xbff763e8)
a[0][1]=1.000000 (&0xbff763f0)
a[1][0]=2.000000 (&0xbff763f8)
a[1][1]=3.000000 (&0xbff76400)
a[2][0]=4.000000 (&0xbff78408)
a[2][1]=5.000000 (&0xbff78410)
3.2 配列が関数の引数に宣言された場合
関数に配列を引数宣言することは可能ですが,実際に
はポインタに置き換えるという規則が適用されます.すな
わち,
func(double a[100]) −→ func(double *a)
と解釈されるのです.これより要素数を宣言することも無
意味である(可読性を高めるという意味はありますが,保
証はされないということ)ことも判ります.実際,aが 1
次元配列ならばその型が判れば i番めの要素は*(a+i) で
アクセスできます.iが範囲を越えるソースを記述しても,
コンパイラは警告を発しません,プログラマーが管理しな
ければならないのです.
しかし,ポインタへの置き換えは 1段しか適用されない
ので,2次元配列の最後の要素数は省略できません.
目次へ5
Cプログラミング 3 配列とポインタ
func(double a[100][1000])
−→ func(double a[][]) 8−→ func(double a[][1000]) ,
例題 5 関数の引数に行数を省略した 2次元配列を宣言
し,関数への実引数にいろいろな配列を渡してみるプログ
ラムを作成しなさい.
[解] プログラムソース例を以下に示します.
リスト 7 arg2darray.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4
5 void f unc ( double a [ ] [ 1 0 0 0 ] )6 {7 p r i n t f ( "% 6.1f (&%p)\n" , a [ 0 ] [ 0 ] , &a [ 0 ] [ 0 ] ) ;8 p r i n t f ( "% 6.1f (&%p)\n" , a [ 0 ] [ 1 0 0 0 ] , &a [ 0 ] [ 1 0 0 0 ] ) ;9 }
10
11 i n t main ( )12 {13 double x [ 2 ] [ 1 0 0 0 ]= { { 2 } , { 0 . 5 } } , y [ 1 0 0 0 ]= {1 0 0 } ;14
15 f unc ( x ) ;16 f unc ( y ) ;17
18 re turn ( 0 ) ;19 }
コンパイル時には,以下のように関数 funcへ渡す引数
のポインタの型が違う(16行なので,配列 y)という警告
がでますが,バイナリができてしまい,実行時にセグメン
テンションフォールトすることもありません.
arg2darray.c:16:警 告: passing argument
1 of ’func’ from incompatible pointer
type
$ ./arg2darray
2.0 (&0xbfe42c80)
0.5 (&0xbfe44bc0)
100.0 (&0xbfe40d40)
2.0 (&0xbfe42c80)
3.3 任意の大きさの 2次元配列
解決すべき問題の行列の大きさが判っている場合には,
関数の引数ではその大きさの配列を宣言するのが自然です
が,いろいろの大きさが混じる場合にその大きさごとの関
数を用意しなければならず,大変苦労します.機能が同じ
ならば,大きさに依らない関数を作成したいと思うのは当
然のなりゆきです.引数に 2次元配列を宣言する方法では,
列の要素数を正定数宣言しなければならないので,この目
標を実現できません.そこで,明示的に宣言された 2次元
配列を 1次元配列とみなしてアクセスする方法が役立ちま
す.すなわち,M × Nの行列を,明示的に a[M][N];と宣
言した場合,それを M × Nの大きさの 1次元配列 a[M*N]
とみなし,
a[i][j] =⇒ a[N*i + j]
で読み書きすることが可能であることを利用します.た
だし,この場合かなり可読性が下がります.可読性をとる
か,汎用性(効率)をとるか,プログラマーは選択を迫ら
れます.
例えば,つぎのようなプロトタイプが考えられます.
func (int m, int n, double a[])
{
...
for (i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
...a[n*i+j]...
}
}
}
2次元配列の大きさ(行数と列数)の情報はを n,mに持た
せます.すると,例えば繰り返しの上限値として用いるこ
とができるようになります.
関数内で 2次元配列表現 a[i][j]で読み書き可能な場
合,すなわち可読性が高いソースでもオーバーフロー防止
は,結局のところプログラマー任せであり,宣言時に要素
数を与えることはオーバーフロー防止に関しては,あまり
意味がありません.なによりも,宣言した要素数の値を関
数内部で取得することができないという問題があります.
したがって,2次元配列の明示的な宣言に拘らずに,1次
元配列とみなして扱う方法を次節以降は多用します.
例題 6 二次元正方行列 Aとベクトル xとの積 Axを計
算する関数を考えなさい.扱う大きさを自由に変更できる
よう,引数を 1次元配列として宣言してみなさい.
[解] プログラムソース例を以下に示します.
リスト 8 myax.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4
5 void myax ( i n t n , double a [ ] , double x [ ] , double y [ ] )6 {7 i n t i , k ;8
9 f o r ( i = 0 ; i < n ; i ++) {10 y [ i ] = 0 ;11 f o r ( k = 0 ; k < n ; k++) y [ i ] += a [ n∗ i+k ]∗ x [ k ] ;12 }13 }14
15 i n t main ( )16 {17 i n t i ;18 double A[ 3 ] [ 3 ] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } , { 7 , 8 , 9 } } ;19 double x [ 3 ] = { 1 0 , 2 0 , 3 0 } ;20 double y [ 3 ] ;21
22 myax ( 3 , ( double ∗ )A, x , y ) ;
目次へ6
Cプログラミング 3 配列とポインタ
23 f o r ( i = 0 ; i < 3 ; i ++) p r i n t f ( "%f\n" , y [ i ] ) ;24
25 re turn 0 ;26 }
3.3.1 転置行列
2 次元行列を引数にとる関数をいくつか作成してみま
しょう.
例題 7 N 次正方行列 Aを引数にとり,Aを転置行列 AT
に書き換える関数を作成して,機能を確かめるプログラム
を作成しなさい.
[解] 解答例を以下に示します.
リスト 9 transp.c
1 # i n c l u d e "lineq.h"
2
3 # d e f i n e N 54
5 void t r a n s p o s e m a t ( Mat A, i n t n )6 {7 i n t i , j ;8 double tmp ;9
10 f o r ( i = 0 ; i < n ; i ++) {11 f o r ( j = 0 ; j < i ; j++ ) {12 tmp = A[ n∗ i+ j ] ;13 A[ n∗ i+ j ] = A[ j ∗n+ i ] ;14 A[ j ∗n+ i ] = tmp ;15 }16 }17 }18
19 i n t main ( i n t argc , char ∗∗ a r gv )20 {21 i n t i , j ;22 double A[N] [N ] ;23
24 f o r ( i = 0 ; i < N; i ++) {25 f o r ( j = 0 ; j < N; j++ ) {26 A[ i ] [ j ] = N∗ i + j ;27 }28 }29
30 p r i n t m a t ( "A =" , ( Mat )A, N, N ) ;31 t r a n s p o s e m a t ( ( Mat )A,N ) ;32 p r i n t m a t ( "AˆT =" , ( Mat )A, N, N ) ;33
34 re turn ( 0 ) ;35 }
行列の表示など雑多な関数がどうしても必要となるのでそ
れらの共通関数郡をまとめて定義したファイルmat aux(aux
は auxiliaryの略)と共通のヘッダファイル lineq.hを用意
しました.
リスト10 lineq.h
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < f l o a t . h>5
6 t y p e d e f double ∗ Mat ;7 t y p e d e f double ∗ Vec ;8
9 void m a t g a u s s ( Mat , Vec , Vec , i n t ) ;10 void m a t g a u s s p i v o t ( Mat , Vec , Vec , i n t ) ;11 void get PLU ( Mat , i n t ∗ , i n t ) ;12 void mat PLU ( Mat , Vec , Vec , i n t ∗ , i n t ) ;13 void L U f a c t o r ( Mat , Mat , Mat , i n t ) ;14
15 void copy mat ( Mat , Mat , i n t , i n t ) ;16 void c o p y m a t t ( Mat , Mat , i n t , i n t ) ;
17 void p r i n t m a t ( char ∗ , Mat , i n t , i n t ) ;18 void p r i n t m a t z ( char ∗ , Mat , i n t , i n t ) ;19 void p r i n t v e c ( char ∗ , Mat , i n t ) ;20 void c a l c a x ( Mat , Vec , Vec , i n t ) ;21 void p r o d u c t m a t ( Mat , Mat , Mat , i n t , i n t , i n t ) ;22 void i n v m a t ( Mat , i n t ) ;
リスト11 mat aux.c
1 # i n c l u d e "lineq.h"
2
3 void copy mat ( Mat s r c , Mat d e s t , i n t m, i n t n )4 {5 i n t i , j ;6
7 f o r ( i = 0 ; i < m; i ++) {8 f o r ( j = 0 ; j < n ; j ++) {9 d e s t [ n∗ i+ j ] = s r c [ n∗ i+ j ] ;
10 }11 }12 }13
14 void c o p y m a t t ( Mat s r c , Mat d e s t , i n t m, i n t n )15 {16 i n t i , j ;17
18 f o r ( i = 0 ; i < m; i ++) {19 f o r ( j = 0 ; j < n ; j ++) {20 d e s t [ i+m∗ j ] = s r c [ n∗ i+ j ] ;21 }22 }23 }24
25 void p r i n t m a t ( char ∗name , Mat A, i n t m, i n t n )26 {27 i n t i , j ;28
29 p r i n t f ( "\n%s\n" , name ) ;30 f o r ( i = 0 ; i < m; i ++) {31 f o r ( j = 0 ; j < n ; j ++) {32 p r i n t f ( " %-9.3g" , A[ n∗ i+ j ] ) ;33 }34 p r i n t f ( "\n" ) ;35 }36 }37
38 void p r i n t v e c ( char ∗name , Vec x , i n t n )39 {40 i n t i ;41
42 p r i n t f ( "\n%s\n" , name ) ;43 f o r ( i = 0 ; i < n ; i ++) p r i n t f ( " %-9.3g" , x [ i ] ) ;44 p r i n t f ( "\n" ) ;45 }46
47 void c a l c a x ( Mat A, Vec x , Vec Ax , i n t n )48 {49 i n t i , j ;50 f o r ( i = 0 ; i < n ; i ++) {51 Ax [ i ] = 0 . 0 ;52 f o r ( j = 0 ; j < n ; j ++) Ax [ i ] += A[ n∗ i+ j ]∗ x [ j ] ;53 }54 }55
56 void p r o d u c t m a t ( Mat A, Mat B , Mat AB,57 i n t m, i n t km , i n t n )58 { /* return AB(M,N)=A(M,L)B(L,N) */
59 i n t i , j , k ;60
61 f o r ( i = 0 ; i < m; i ++){62 f o r ( j = 0 ; j < n ; j ++) {63 AB[ n∗ i + j ] = 0 ;64 f o r ( k = 0 ; k < km ; k++) {65 AB[ n∗ i + j ] += A[km∗ i + k ]∗B[ n∗k + j ] ;66 }67 }68 }69 }70
71 double z ( double x )72 {73 re turn f a b s ( x ) < DBL EPSILON ? 0 : x ;74 }75
76 void p r i n t m a t z ( char ∗name , Mat s r c , i n t m, i n t n )77 {78 i n t i , j ;79
目次へ7
Cプログラミング 4 連立方程式の解法と行列
80 p r i n t f ( "\n%s\n" , name ) ;81 f o r ( i = 0 ; i < m; i ++) {82 f o r ( j = 0 ; j < n ; j ++) {83 p r i n t f ( " %-9.3g" , z ( s r c [ n∗ i+ j ] ) ) ;84 }85 p r i n t f ( "\n" ) ;86 }87 }
2次元配列(の名前)を doubleのポインタと解釈するこ
とに決めて,Matという型を typedefしています.引数の
型に論理的な名称を用いる(つまり行列であるを示す)こ
とで,少し可読性が高くなっていると思います.
次のようにコンパイルしてください.� �
$ gcc -c mat aux.c��
��
$ gcc -o transp transp.c mat aux.o -Wall��
��
� �
実行画面例を以下に示します.
$ gcc -c mat_aux.c
$ gcc -o transp transp.c mat_aux.o -Wall
$ ./transp
A =
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
AˆT =
0 5 10 15 20
1 6 11 16 21
2 7 12 17 22
3 8 13 18 23
4 9 14 19 24
4 連立方程式の解法と行列
工学上のいろいろな問題を数値計算してみようとすると,
連立一次方程式
a11x1 + a12x2 + a13x3 + · · · + a1nxn = b1
a21x1 + a22x2 + a13x3 + · · · + a2nxn = b2
· · ·an1x1 + an2x2 + a23x3 + · · · + annxn = bn
(1)
を解くことに帰着される場合が多々あります.したがって
連立一次方程式を精度よくかつ高速に解く方法が考案され
てきました.ここでは,その中でも最も基本的なガウス消
去法と LU分解法のアルゴリズムを勉強しましょう.
この方程式は以下のように,係数行列 Aと解ベクトル x
および定数ベクトル bを用いて
Ax = b (2)
A =
a11 a12 · · · ann
a21 a22 · · ·...
....... . .
...
an1 an2 · · · ann
, b =
b1
b2
...
bn
, x =
x1
x2
...
xn
(3)
と表されます.
4.1 ガウスの消去法
中学校等で習った三元連立一次方程式方程式の解法を思
い出してください.式に定数をかけて両辺の差をとって,
変数を減らしていくという方法を習いました.ガウスの消
去法は,この方法と同じく「式を変形して差をとり変数を
減らしていく」方法です.式 (1)の第 1式を用いて,第 2
式以下の第 i式の x1の項を消去するには,第 1式を ai1/a11
倍して辺々引けばよいことは明らかです.このようにして
第 2式以下第 i (i ≥ 2)式はn
∑
j=2
(
ai j −ai1
a11
a1 j
)
x j = bi −ai1
a11
b1 (4)
と変形されますが,変数が 1つ減った (n−1)個の式が得ら
れます.ます.この n − 1個の式の最初の式を除いた n − 2
個の式に対して上記と同じ変形を行って x2の項を消去し,
以下同様に n − 1繰り返します.最後には,元の n次元連
立方程式は以下のような連立方程式に変換されます.
a11x1 + a12x2 + a13x3 + · · · + a1nxn = b1
a(1)22
x2 + a(1)23
x3 + · · · + a(1)2n
xn = b(1)2
· · ·n
∑
j=i
a(i−1)i j
x j = b(i−1)i
· · ·a
(n−2)n−1,n−1
xn−1 + a(n−2)n−1,n
xn = b(n−2)n−1
a(n−1)n,n xn = b(n−1)
n
(5)
目次へ8
Cプログラミング 4 連立方程式の解法と行列
ここで係数の右肩にある (i)は,i回の変形により変換され
て元の係数とは異なっていることを明示的に示したもので
す.このように変数を消去する方法を前進消去と呼びます.
さてこの連立方程式 (5)の一番下の式,すなわち第 n式
から xnが求まります.それを第 n− 1式に代入して xn−1が
求まり,xn, xn−1を第 n−2式に代入して xn−2が求まり,以
下繰り返して全ての xi (1 ≤ i ≤ n)が求まります.このよう
に,nの大きな値から遡って代入を繰り返す方法を後退代
入と呼びます.一般式の詳細は参考書で各自学習してくだ
さい.
前進消去と後退代入をC言語で書き表した(コード化し
た)ものを以下に示します.
for (k = 0; i < n-1; k++) {for (i = k+1; i < n; i++) {r = a[i][k]/a[k][k];
for (j = k+1; j < n; j++) a[i][j]-=r*a[k][j];
b[i] = b[i] - r*b[i];
}}for (i = n-1; i >= 0; i--) {sum =0;
for (j = i+1; j < n; j++) sum += a[i][j]*x[j];
x[i] = (b[i] - sum)/a[i][i];
}
4.1.1 1次元配列化とメモリ確保
ところで,実際にソースコードを書いて適当な行列を入
れてテストしようとすると,はたと困ることが起きます.
C言語では,2次元配列は列の大きさを宣言しなければな
らないので,係数行列の大きさ,すなわち連立方程式の元
の数をソースに定数として宣言しなければならないのです.
これでは,元の数に応じて関連する関数の仮引数の 2次元
行列の大きさの宣言部分を書き直さなければなりません.
そこで,
+「1次元配列」を「2次元配列」とみなして
アクアセスする
方法を利用して,行列の大きさによらない汎用のコードを
作成しましょう.C言語では,一次元配列は動的に領域確
保ができます.そのための関数がmalloc(), calloc()です.
例題 8 ガウスの消去法による連立 n次元方程式の解法
を確かめるプログラムを作成しなさい.ただし,連立方程
式の次元数 nと,係数行列 A,定数ベクトル bは,引数に
指定したファイルから読み込む仕様としなさい.
[解] 解答例とその概略を以下に示します.
リスト12 lineq gauss.c
1 # i n c l u d e "lineq.h"
2
3 i n t main ( i n t argc , char ∗ a r gv [ ] )4 {5 i n t N, i , j ;6 Mat A, Ai ;7 Vec b , x , Ax ;8 FILE ∗ f i n ;9
10 i f ( a r g c < 2) {11 p r i n t f ( "Usage: %s datafile\n" , a r gv [ 0 ] ) ;12 e x i t ( 0 ) ;13 }14
15 f i n = fopen ( a r gv [ 1 ] , "r" ) ;16 f s c a n f ( f i n , "%d" , &N ) ;17 A = c a l l o c ( N∗N, s i z e o f ( double ) ) ;18 Ai = c a l l o c ( N∗N, s i z e o f ( double ) ) ;19 b = c a l l o c ( N, s i z e o f ( double ) ) ;20 x = c a l l o c ( N, s i z e o f ( double ) ) ;21 Ax = c a l l o c ( N, s i z e o f ( double ) ) ;22
23 f o r ( i = 0 ; i < N; i ++) {24 f o r ( j = 0 ; j < N; j ++) {25 f s c a n f ( f i n , "%lf" , &A[N∗ i+ j ] ) ;26 }27 }28 f o r ( i = 0 ; i < N; i ++) {29 f s c a n f ( f i n , "%lf" , &b [ i ] ) ;30 }31
32 copy mat (A, Ai , N, N ) ;33 p r i n t m a t ( "A =" , A, N, N ) ;34 p r i n t v e c ( "b =" , b , N ) ;35
36 m a t g a u s s (A, b , x , N ) ;37 p r i n t v e c ( "x =" , x , N ) ;38 c a l c a x ( Ai , x , Ax , N ) ;39 p r i n t v e c ( "Ax = b?" , Ax , N ) ;40
41 f c l o s e ( f i n ) ;42 f r e e (A ) ; f r e e ( Ai ) ; f r e e ( b ) ; f r e e ( x ) ; f r e e ( Ax ) ;43
44 re turn 0 ;45 }
メイン関数の引数の数を調べて,引数が 1つあった場合
(文字列 argv[1]に格納される)その名前でファイルをオー
プンします(15行目).行列のデータファイルの 1行目に
は元の数 nを記述する仕様として,最初に fscanf()を用い
て,元の数を変数 N に読み込みます(16 行目).17~20
行目で,2次元行列やベクトルとして Matや Vecで宣言し
たポインタ変数 A, Ai, b, x, Axに領域を確保して実体
を与えています.
void *calloc(size t nmemb, size t size);
void *malloc(size t size);
boid free(void *ptr);
+ calloc()は,オブジェクトのサイズ size(例えば
doubleならば 4)とその要素数 nmembを指定して連続し
た領域を確保し,その先頭アドレスを返却します.その際,
領域は 0で初期化されます.mallocは,オブジェクトのサ
イズのみを指定して連続した領域を確保し,ポインタ値を
返却します.calloc()のパラメータでいえば,nmemb*size
を指定することになります.初期化はされません.
得られた連続領域を 1次元配列とみなして,読み書きし
ます.22~26行目は係数行列を行毎に読み込んで,配列に
目次へ9
Cプログラミング 4 連立方程式の解法と行列
セットしています.概念的には
A[N*i +j] = A[i][j] <-(代入) ai j
です.fscanf()の引数ですから,(ポインタ宣言してない)変
数にはアドレス演算子 & を前置してポインタを与えなけ
ればならないことを思い起こしてください.27~29行目
は,定数ベクトル b = biが 1行に記述されている(視覚的
には横ベクトルにみえますが,扱いを縦ベクトルにすれば
いいだけですので問題ありません)として読み込みます.
係数行列 Aと定数ベクトル bを読み込んだら,それを
表示し(print mat, print vec等は ”lineq.h”で定義して
いる),ガウスの消去法による解法関数 mat gaussに渡し
て,解ベクトル xを得ます.
ガウスの消去法による解法関数 mat gaussは別ファイル
に作成し,分割コンパイルします.
リスト13 mat gauss.c
1 # i n c l u d e "lineq.h"
2
3 void m a t g a u s s ( Mat A, Vec b , Vec x , i n t n )4 {5 i n t i , j , k ;6 double r , s , ∗ a [ n ] ;7
8 f o r ( i = 0 ; i < n ; i ++) a [ i ] = A + n∗ i ;9
10 f o r ( k = 0 ; k < n−1; k++) {11 f o r ( i = k+1; i < n ; i ++) {12 r = a [ i ] [ k ] / a [ k ] [ k ] ;13 f o r ( j = k+1; j < n ; j ++) a [ i ] [ j ] −= r ∗ a [ k ] [ j ] ;14 b [ i ] −= r ∗b [ k ] ;15 }16 }17
18 f o r ( i = n−1; i >=0; i −−) {19 s = 0 ;20 f o r ( j = i +1; j < n ; j ++) s += a [ i ] [ j ]∗ x [ j ] ;21 x [ i ] = ( b [ i ]− s ) / a [ i ] [ i ] ;22 }23 }
a[i][j]で記述できるように(12,13,20,21行目),わざわ
ざポインタ変数配列 double *a[n]を宣言して(6行目),
1次元配列の先頭アドレスを代入しています(8行目).動
作時に確定した正定数を用いて動的に配列を確保できる機
能は,可変長配列と呼ばれ,C99で標準化されました.gcc
はこの機能を実装しています.
分割コンパイルのための makefile.gaussを示します.
makefile名を指定するオプション-f makefileを用いて� �
make -f makefile.gauss��
��
� �
とすれば,関連ファイルが一気にできあがります.
リスト14 makefile.gauss
1 CC = gcc2 OBJS = m a t g a u s s . o mat aux . o3 BINS = l i n e q g a u s s4 CCFLAGS = −O2 −Wall5 LIBS = −lm6
7 l i n e q g a u s s : l i n e q g a u s s . c $ ( OBJS )8 $ (CC) $ (CCFLAGS) −o $@ $ ˆ $ ( LIBS )9 %.o : %.c
10 $ (CC) $ (CCFLAGS) −c −o $@ $<11 c l e a n :12 rm $ ( OBJS ) $ ( BINS ) ∗ ˜
次のような 3元連立方程式のデータファイルを用意して,
lineqに読み込ませましょう.
リスト15 matrix.dat
1 3
2 1 2 3
3 2 8 11
4 3 22 35
5 -1 0 1
実行画面を示します.
$ ./lineq_gauss matrix.dat
A =1 2 32 8 113 22 35
B =-1 0 1
x =-1.667 1.333 -0.6667
Ax = B?-1 1.11e-16 1
最後の行で検算を行っています.求めた解ベクトル xと
係数行列 Aの積 Axが bと等しくなっていることを確認し
てください.第 2成分が 1.11e-16となっていますが,こ
れは 0とみなしていいでしょう.
問題 1 Hilbert行列,Pascal行列などの定義を調べて,そ
れらの行列を用いた連立 1次方程式の問題を考えなさい.
4.1.2 ピボット選択
変数の消去を第 1列目から実行していくと,時々除算に
使う係数の値が 0となってしまう合があります.元来,第
1列目から順番に消去を行う必要はない(プログラムは簡
素にはなりますが)ので,そのような場合には 0でない係
数の式を選択すればよいことになります.このような操作
を部分ピボット選択と呼びます.ついでに,絶対値の大き
なものを選ぶことにすると,誤差の伝搬を小さく抑えるこ
とができるようになります.
問題 2 ガウス消去法にピボット選択のルーチンを追加し
た関数 mat gauss pivot()を作成し,次の連立一次方程
式を解かせてその効果を確かめなさい.
Ax = b, A =
−1 3 2 5
2 −6 4 7
3 4 5 6
4 3 2 1
, b =
9
−7
−6
−8
2の解 コード化の例を以下に示します.係数が 0であ
るという判別に用いる小さな数(倍精度実数)εとして,
1 + ε > 1
目次へ10
Cプログラミング 4 連立方程式の解法と行列
を満たす最小の正数を用いました.一般にマシンエプシ
ロンと呼ばれるこの数は,ヘッダーファイル float.hに
DBL EPSILONとして定義されています.
リスト16 mat gauss pivot.c
1 # i n c l u d e "lineq.h"
2
3 void m a t g a u s s p i v o t ( Mat A, Vec b , Vec x , i n t n )4 {5 i n t i , j , k , p ;6 double r , s , tmp , ∗ a [ n ] ;7
8 f o r ( i = 0 ; i < n ; i ++) a [ i ] = A + n∗ i ;9
10 f o r ( k = 0 ; k < n−1; k++) {11 p = k ;12 f o r ( i = k ; i < n ; i ++) {13 i f ( f a b s ( a [ p ] [ k ] ) < f a b s ( a [ i ] [ k ] ) ) p = i ;14 }15
16 i f ( f a b s ( a [ p ] [ k ] ) < DBL EPSILON ) {17 p r i n t f ( "failure\n" ) ;18 e x i t ( 0 ) ;19 }20
21 i f ( p != k ) {22 f o r ( j = k ; j < n ; j ++) {23 tmp = a [ k ] [ j ] ;24 a [ k ] [ j ] = a [ p ] [ j ] ;25 a [ p ] [ j ] = tmp ;26 }27 tmp = b [ k ] ;28 b [ k ] = b [ p ] ;29 b [ p ] = tmp ;30 }31
32 f o r ( i = k+1; i < n ; i ++) {33 r = a [ i ] [ k ] / a [ k ] [ k ] ;34 f o r ( j = k+1; j < n ; j ++) a [ i ] [ j ] −= r ∗ a [ k ] [ j ] ;35 b [ i ] −= r ∗b [ k ] ;36 }37 }38
39 f o r ( i = n−1; i >=0; i −−) {40 s = 0 ;41 f o r ( j = i +1; j <n ; j ++) s += a [ i ] [ j ]∗ x [ j ] ;42 x [ i ] = ( b [ i ]− s ) / a [ i ] [ i ] ;43 }44 }
ピボット選択がない場合(lineq gauss)と有る場合
(lineq pivot)の実行結果を以下に示します.ピボット
選択なしでは nanが発生してしまいますが,ピボット選択
ありでは正しい計算結果を得ます.
$ ./lineq_gauss matrix_pivot.dat
A =2 4 1 -3-1 -2 2 44 2 -3 55 -4 -3 1
B =0 10 2 6
x =nan nan nan nan
Ax = B?nan nan nan nan
�画面中の nan は,NaN(Not a Number) のことです.主として浮動小
数点演算の経過の中で,不正なオペランドが与えられたために通常の浮動小数点数や無限大(inf)で表せない結果を生じたことを示しています.例えば,0/0,0×∞,
√−1,arcsin(10) etc.が演算にかかった場合等に
nan が返される可能性があります.例外を発生させて計算を中断するよりも nan を返して最後まで計算を続行させる,あるいは結果が nan であるか検査して(isnan() 関数)条件分岐さる方がデバッグに有利となる
ため,実装が進んでいるようです.例えば,C99では表 1に示すように,浮動小数点数の分類について 5 つのカテゴリを設けてシンボルを定義しています.
表 1 浮動小数点のカテゴリ ∗C99
マクロ名 カテゴリFP ZERO 値 0 の浮動小数点数FP NORMAL 正規化表現の浮動小数点数FP SUBNORMAL 非正規表現の浮動小数点数FP INFINITE 無限大の値を表す浮動小数点数FP NAN 数を表現しないビットパターン
+参考ウェブサイト: IEEE754 の規格(2 進数浮動小数点数の規格
のほぼ標準)
$ ./lineq_pivot matrix_pivot.dat
A =2 4 1 -3-1 -2 2 44 2 -3 55 -4 -3 1
B =0 10 2 6
x =2 -1 3 1
Ax = B?-8.88e-16 10 2 6
4.1.3 可変長配列:C99の新機能
C99では,配列のサイズ指定に変数を用いることができ
る,いわゆる可変長配列 (Variable Length Array)が認め
られました.この機能を使えば,2次元配列を 1次元配列
として扱うなどといった技を駆使せずに,素直なコードを
記述できます.
リスト17 lineq array.c
1 # i n c l u d e "lineq.h"
2
3 i n t main ( i n t argc , char ∗ a r gv [ ] )4 {5 i n t N, i , j ;6 FILE ∗ f i n ;7
8 i f ( a r g c < 2) {9 p r i n t f ( "Usage: %s datafile\n" , a r gv [ 0 ] ) ;
10 e x i t ( 0 ) ;11 }12
13 f i n = fopen ( a r gv [ 1 ] , "r" ) ;14 f s c a n f ( f i n , "%d" , &N ) ;15
16 /* C99 では許された宣言 */
17 double A[N] [N] , Ai [N] [N] , b [N] , x [N] , Ax [N ] ;18
19 f o r ( i = 0 ; i < N; i ++) {20 f o r ( j = 0 ; j < N; j ++) {21 f s c a n f ( f i n , "%lf" , &Ai [ i ] [ j ] ) ;22 }23 }24 f o r ( i = 0 ; i < N; i ++) {25 f s c a n f ( f i n , "%lf" , &b [ i ] ) ;26 }27 f c l o s e ( f i n ) ;28
29 p r i n t m a t ( "A = " , ( Mat ) Ai , N, N ) ;30 p r i n t v e c ( "b = " , b , N ) ;31 copy mat ( ( Mat ) Ai , ( Mat )A, N, N ) ;32 m a t g a u s s p i v o t ( ( Mat )A, ( Vec ) b , ( Vec ) x , N ) ;
目次へ11
Cプログラミング 4 連立方程式の解法と行列
33 p r i n t v e c ( "x =" , ( Vec ) x , N ) ;34 c a l c a x ( ( Mat ) Ai , ( Vec ) x , ( Mat ) Ax , N ) ;35 p r i n t v e c ( "Ax = b?" , ( Vec ) Ax , N ) ;36
37 re turn 0 ;38 }
14行目で実行時にサイズ Nをデータファイルから取得
して,それを元に動的に配列を宣言しています.このおか
げで,21行目の 2次元配列 Aiへの代入が自然な形で記述
できます.配列名を print mat等に渡そうとすると,互換
性のないポインタ (Imcompatible pointer)が引きわたされ
たという警告が出てしまう(コンパイルは成功しますし,
ちゃんと動作しますので手当しなくても構いません)ので,
見た目をごまかすために,print mat等の宣言に合わせて
(Mat)にキャストしています.実質的には,次の例のよう
に無理矢理 doubleへのポインタにキャストしておいて呼
出しをかけて誤魔化しているわけです.
mat gauss((double*)A, (double*)b, N)
なお,可変長配列のサイズを指定する整数変数は,定義さ
れていなければならないので,mat gaussなどの関数を
mat gauss(double a[n][n], double b[n], int n) 8
と宣言することはできません.変数 nが未定義(ここで初
めて現れている)だからです.
4.2 LU分解法
ガウスの消去法で連立 1次方程式
Ax = b (6)
を解く過程を振り返ってみましょう.ただし,Aは係数行
列,x, bそれぞれ解ベクトルと定数ベクトルです.
4.2.1 上三角行列 U と下三角行列 L
方程式は最終的に次のような,対角線より左下部分が全
て 0である,いわゆる下三角行列 U で表される行列を含
む形に変換されました(式 (5)参照).
u11 u12 · · · u1n
u22 · · · u2n
. . ....
unn
x1
x2
...
xn
=
b′1
b′2...
b′n
(7)
0
そして,この形の場合には解ベクトル xを xnの方から遡っ
て求めることができること(後退代入)はガウスの消去法
で示した通りです.これを行列で表せば,
x = U−1b′ (8)
となり,すなわちこ b′ から xを算出する手順をコード化
することができた(すなわちプログラムのソースが記述で
きる)ということになります(U−1自体が求まったという
ことではありません).また,係数行列 Aを上三角行列に
置換する計算手順も判っています (前進消去)から,これは
すなわち
S A(= U), S b(= b′) (9)
を算出する手順もコード化できていることになります.
さて,変換行列 S は実は,係数を消去する行列 L−1と行
の入れ換え全過程(部分ピボット選択)を表す行列 Pとの
積に分解でき,
S = L−1P (10)
しかも Lは次のような対角成分が全て 1である下三角行列
(対角線より右上部分が全て 0である行列)
L =
1
l12 1...
.... . .
ln1 ln2 · · · 1
(11)0
となることが示されます(詳細は参考書).
目次へ12
Cプログラミング 4 連立方程式の解法と行列
以上より,連立 1次方程式 (6)の解ベクトルを求める手
順をもう一度整理してみると.係数行列 Aを上三角行列U
に変換する行列 S = L−1Pがそれぞれ bに無関係に定まり
y = Ux = S Ax = S b (12)
とおくと,
x = U−1y = U−1S b = U−1L−1Pb (13)
と表されます.計算方法がコード化されている定数ベクト
ルへの左側からの S や U−1 の積を用いていることに注目
しましょう.最後の式から
LUx = Pb = b′ (14)
これは,定数ベクトル b′ に対する連立 1次方程式の係数
行列 PAを Lと U の積に分解したことになります.PAが
Lと U の積で表されるかどうかは Aによりますが,少な
くともガウスの消去法で解ける場合にはこの LU分解が可
能であることは自明です.
実際のコードの主要部分を以下に示します.ほとんどガ
ウスの消去法と同じですが,ai j に対する操作を置換を含
めて記録しておき,後で bi に記録した操作を実施するよ
うになっています.
for (k = 0; k < n-1; k++) {p = k;
for (i = k; i < n; i++) {if(fabs(a[p][k]) < fabs(a[i][k])) p = i;
}if (fabs(a[p][k]) < DBL EPSILON) printf("failure\n");
p[k] = p; /* 置換行の記録 */if (p != k) {for (j = k; j < n; j++) {c = a[k][j]; a[k][j] = a[p][j]; a[p][j] = c;
}}for (i = k+1; i < n; i++) {r = a[i][k]/a[k][k];
a[i*n + k] = r;
for (j = k+1; j < n; j++) a[i][j] -= r*a[k][j];
}}
/* b[i] に対する操作を分離 */for (k = 0; k < n-1; k++) {c = b[k]; b[k] = b[p[k]]; b[p[k]] = c;
for (i = k+1; l < n; i++) {b[i] -= a[i][k]*b[k];
/* a[k+1][k](本来は 0) に係数を記録してあった */}
}
/* 後退代入 */for (i = n-1; i => 0; i--) {sum = 0;
for (j = i+1; j < n; j++) sum += a[i][j]*b[j];
b[i] = (b[i] - sum)/a[i][i];
}
例題 9 LU 分解法を用いて,次の連立 1次方程式を解き
なさい.
Ax = b, A =
10 −2 −1 1
−2 10 1 1
−1 1 10 −2
1 1 −2 −10
, b =
24
14
11
13
[解] まず LU 分解法を,係数行列 Aに対する部分ピボッ
ト選択付きの前進消去の部分 get PLU(· · · )と,定数ベクトル bに対する部分ピボット選択付きの前進消去および後
退代入 mat PLU(· · · )の 2つ関数に分けます.
リスト18 mat LU.c
1 # i n c l u d e "lineq.h"
2
3 void get PLU ( Mat a , i n t ∗ pvt , i n t n )4 {5 i n t i , j , k , p ;6 double c , r ;7
8 f o r ( k = 0 ; k < n−1 ; k++) {9 p = k ;
10 f o r ( i = k ; i < n ; i ++) {11 i f ( f a b s ( a [ p∗n+k ] ) < f a b s ( a [ i ∗n+k ] ) ) {12 p = i ;13 }14 }15 i f ( f a b s ( a [ p∗n+k ] ) < DBL EPSILON ) {16 p r i n t f ( "failure\n" ) ;17 e x i t ( 0 ) ;18 }19 p v t [ k ] = p ;20 i f ( p != k ) {21 f o r ( j = k ; j < n ; j ++) {22 c = a [ k∗n+ j ] ;23 a [ k∗n+ j ] = a [ p∗n+ j ] ;24 a [ p∗n+ j ] = c ;25 }26 }27
28 f o r ( i = k+1; i < n ; i ++) {29 r = a [ i ∗n+k ] / a [ k∗n+k ] ;30 a [ i ∗n+k ] = r ;31 f o r ( j = k+1; j < n ; j ++) {32 a [ i ∗n+ j ] −= r ∗ a [ k∗n+ j ] ;33 }34 }35 }36 }37
38 void mat PLU ( Mat a , Vec b , Vec x , i n t ∗ pvt , i n t n )39 {40 i n t i , j , k ;41 double c , s ;42
43 f o r ( k = 0 ; k < n − 1 ; k++) {44 c = b [ k ] ;45 b [ k ] = b [ p v t [ k ] ] ;46 b [ p v t [ k ] ] = c ;47 f o r ( i = k + 1 ; i < n ; i ++) {48 b [ i ] −= a [ i ∗n+k ] ∗ b [ k ] ;49 }50 }51
52 f o r ( i = n − 1 ; i >= 0 ; i −−) {53 s = 0 ;54 f o r ( j = i + 1 ; j < n ; j ++) {55 s += a [ i ∗n+ j ] ∗ x [ j ] ;56 }57 x [ i ] = ( b [ i ] − s ) / a [ i ∗n+ i ] ;58 }59 }
配列 pvtに置換の情報を書き込んで返します(19行目).
また,変換された係数行列は上三角部分のみが後退代入の
計算に使われます.そこで bの変換際に必要な係数は,A
の対角線のすぐ下 ak+1 k に保存しておきます(30行目).
目次へ13
Cプログラミング 4 連立方程式の解法と行列
LU 分解法を確かめるためのテストプログラム例を以下
に示します.
リスト19 lineq LU.c
1 # i n c l u d e "lineq.h"
2
3 i n t main ( i n t argc , char ∗ a r gv [ ] )4 {5 i n t N, i , j , ∗PVT ;6 Mat A;7 Vec b , x ;8 FILE ∗ f i n ;9
10 i f ( a r g c < 2) {11 p r i n t f ( "Usage: %s datafile\n" , a r gv [ 0 ] ) ;12 e x i t ( 0 ) ;13 }14
15 f i n = fopen ( a r gv [ 1 ] , "r" ) ;16 f s c a n f ( f i n , "%d" , &N ) ;17 A = c a l l o c ( N∗N, s i z e o f ( double ) ) ;18 b = c a l l o c ( N, s i z e o f ( double ) ) ;19 x = c a l l o c ( N, s i z e o f ( double ) ) ;20 PVT = c a l l o c ( N, s i z e o f ( i n t ) ) ;21
22 f o r ( i = 0 ; i < N; i ++) {23 f o r ( j = 0 ; j < N; j ++) {24 f s c a n f ( f i n , "%lf" , &A[N∗ i+ j ] ) ;25 }26 }27 f o r ( i = 0 ; i < N; i ++) {28 f s c a n f ( f i n , "%lf" , &b [ i ] ) ;29 }30
31 p r i n t m a t ( "A =" , A, N, N ) ;32 p r i n t v e c ( "b =" , b , N ) ;33 get PLU (A, PVT , N ) ;34 mat PLU (A, b , x , PVT , N ) ;35 p r i n t v e c ( "x =" , x , N ) ;36
37 f c l o s e ( f i n ) ;38 f r e e (A ) ; f r e e ( b ) ; f r e e ( x ) ; f r e e (PVT ) ;39
40 re turn 0 ;41 }
� �
$ gcc -c mat LU.c��
��
$ gcc -Wall -o lineq LU lineq LU.c mat LU.o
mat aux.o -lm��
��
� �
4.2.2 LU 分解の効能
LU 分解は解法のアルゴリズムとしてはガウスの消去法
そのものですから,計算の効率が高いわけではありません.
しかしながら,P, L, U が係数行列 Aだけに依存して定ま
り,b とは無関係である点が有効に働く場合があります.
つまり,Aは一定で,異なる複数の定数ベクトルに対する
解ベクトルを求めることが必要な場合です.
問題 3 LU分解法を用いて逆行列を求めるプログラムを
作成しなさい.
〈ヒント〉 i番目の成分が 1で残りが全て 0である定数ベ
クトル bi (i = 1 · · · n)を並べると,単位行列 I を構成でき
ます.すると,
AX = A[
x1, · · · , xn
]
=[
b1, · · · , bn
]
= In
より,X = A−1 となります.
4.3 LAPACKの利用
LAPACK(Linear Algebra PACKage:レイパック)は,For-
tran77で書かれた連立 1次方程式,線形最小二乗問題,固
有値問題,特異値問題などの解法を集めた高性能ライブラ
リです.netlibで公開されているものですが,古くから使
われていて信頼性が高いといわれてますから,線形問題の
数値計算に利用して損はありません.
+参考ウェブサイト:LAPACKと BLAS,ATLAS
• Netlibの LAPACK
• Netlibの LAPACK Users’s Guide
• Netlibの BLAS
• Netlibの ATLAS
元々Fortran用に書かれたものなので,C言語から呼び出
す場合にはいくつか注意が必要です.
( i ) C言語用に書き換えられた,CLAPACKでは,関数名
は大文字ではなく,小文字+アンダースコア “ ”とな
ります.
例)DGESV −→ dgesv
( ii ) M × N の 2次元行列の要素をアクセスする場合には,
Fortran流に A[i][j]=A[i+M*j]としなければなりませ
ん (Column-major),
(iii) 関数への引数は全てポインタとなります.行列名はポ
インタですから,そのまま実引数として渡せます.
4.3.1 連立 1次方程式の解法: DGESV
さっそく使ってみましょう.倍精度実数線形方程式を解く
simpleルーチンはDGESVです.最初の 1文字 DはDouble
(倍精度実数)を表します.次の 2文字は行列の種類 (対称
行列,帯行列)を表しますが,GEは Generalの略で対称で
もない,帯でもない普通の行列を意味します.したがって
万能ですが,帯行列専用に比べれば性能は高くありません.
最後の SVが連立 1次方程式の解法を意味します.LAPACK
がインストールされている場合には以下のように,manで
関数の概要を調べられます.� �
man dgesv��
��
� �
DGESV(N, NRHS, A, LDA, IPIV, B, LDB, INFO)
INTEGER INFO, LDA, LDB, N, NRHS
INTEGER IPIV( * )
DOUBLE PRECISION A( LDA, * ), B( LDB, * )
Aは係数行列ですが,返却時には PLU に書き換えられて
しまっています.Bは右辺の定数ベクトル(定数ベクトル
を並べた行列も可能)ですが,返却時には解ベクトルに書
き換えられてしまっています.IPIVはピボット選択の順
目次へ14
Cプログラミング 4 連立方程式の解法と行列
番を格納するベクトルです.N,NHRS,LDA,LDBにより,
これらの行列やベクトルの大きさは次のようになります,
A (LDA×N),B (LDB×MHRS),IPIV,(N)
従って,通常の用途では,
N=N,LDA=LDB=N,NHRS=1
を与えればよいことになります.最後の INFOは計算の成
否を戻します(0で成功).
例題10 LAPACKの DGESV(C言語用の CLAPACKで
は dgesv )を用いて,全節の連立 1次方程式を解くプロ
グラムを作成しなさい.
[解] 解答例を以下の示します.係数行列を転置して渡さなけ
ればならないので,copy mat t(double* src, double*
dest, ...)という関数(36行目)を mat aux.oの中に追
加しました.LAPACKの解法ルーチン dgesv に与える引数
が全てポインタであることに注意してください(39行目).
リスト20 lineq lapack.c
1 # i n c l u d e "lineq.h"
2
3 i n t main ( i n t argc , char ∗ a r gv [ ] )4 {5 i n t i , j , N, NRHS, LDA, LDB, INFO , ∗ IPIV ;6 Mat A, Ai ;7 Vec b ;8 FILE ∗ f i n ;9
10 i f ( a r g c < 2) {11 p r i n t f ( "Usage: %s datafile\n" , a r gv [ 0 ] ) ;12 e x i t ( 0 ) ;13 }14 NRHS=1;15
16 f i n = fopen ( a r gv [ 1 ] , "r" ) ;17 f s c a n f ( f i n , "%d" , &N ) ;18 LDA = LDB = N;19 A = c a l l o c ( N∗LDA, s i z e o f ( double ) ) ;20 Ai = c a l l o c ( N∗LDA, s i z e o f ( double ) ) ;21 b = c a l l o c ( NRHS∗LDB, s i z e o f ( double ) ) ;22 IPIV = c a l l o c ( N, s i z e o f ( i n t ) ) ;23
24 f o r ( i = 0 ; i < LDA; i ++) {25 f o r ( j = 0 ; j < N; j ++) {26 f s c a n f ( f i n , "%lf" , &Ai [N∗ i+ j ] ) ;27 }28 }29 f o r ( i = 0 ; i < LDB; i ++) {30 f s c a n f ( f i n , "%lf" , &b [ i ] ) ;31 }32
33 p r i n t f ( "\n>>> Using LAPACK dgesv_ <<<\n" ) ;34 p r i n t m a t ( "A =" , Ai , LDA, N ) ;35 p r i n t v e c ( "b =" , b , LDB ) ;36
37 c o p y m a t t ( Ai , A, LDA, N ) ;38 d g e s v (&N, &NRHS, A, &LDA, IPIV , b , &LDB, &INFO ) ;39 i f ( INFO == 0) {40 p r i n t v e c ( "x =" , b , N ) ;41 } e l s e {42 p r i n t f ( "Failure\n" ) ;43 }44
45 f c l o s e ( f i n ) ;46 f r e e (A ) ; f r e e ( Ai ) ; f r e e ( b ) ; f r e e ( IPIV ) ;47
48 re turn 0 ;49 }
� �
$ gcc -o lineq lapack lineq lapack.c mat aux.o
-llapack -lm��
��
� �
$ ./lineq_lapack matrix2.dat
A =10 -2 -1 1-2 10 1 1-1 1 10 -21 1 -2 -10
B =24 14 11 13
x =3 2 1 -1
問題 4 dgesv を用いて,逆行列を求めるプログラムを
作成しなさい.
4の解LU分解法の問題のヒントと同様,右辺に単位行
列 I を渡せば,解行列 X が係数行列 Aの逆行列になって
いるはずです.以下に解答例を示しますが,lineq lapack.c
とほとんど変わりません.
リスト21 inv lapack.c
1 # i n c l u d e "lineq.h"
2
3 i n t main ( i n t argc , char ∗ a r gv [ ] )4 {5 i n t i , j , N, NRHS, LDA, LDB, INFO , ∗ IPIV ;6 Mat A, Ai , B , X;7 FILE ∗ f i n ;8
9 i f ( a r g c < 2) {10 p r i n t f ( "Usage: %s datafile\n" , a r gv [ 0 ] ) ;11 e x i t ( 0 ) ;12 }13
14 f i n = fopen ( a r gv [ 1 ] , "r" ) ;15 f s c a n f ( f i n , "%d" , &N ) ;16 LDA = LDB = N;17 NRHS=N;18 A = c a l l o c ( N∗LDA, s i z e o f ( double ) ) ;19 Ai = c a l l o c ( LDA∗N, s i z e o f ( double ) ) ;20 B = c a l l o c ( NRHS∗LDB, s i z e o f ( double ) ) ;21 X = c a l l o c ( LDB∗NRHS, s i z e o f ( double ) ) ;22 IPIV = c a l l o c ( N, s i z e o f ( i n t ) ) ;23
24 f o r ( i = 0 ; i < N; i ++) {25 f o r ( j = 0 ; j < N; j ++) {26 f s c a n f ( f i n , "%lf" , &Ai [N∗ i+ j ] ) ;27 }28 }29 f o r ( i = 0 ; i < N; i ++) {30 B[N∗ i+ i ]=1 ;31 }32
33 p r i n t f ( "\n>>> Using LAPACK dgesv_ <<<\n" ) ;34 c o p y m a t t ( Ai , A, LDA, N ) ;35 p r i n t m a t ( "A =" , Ai , LDA, N ) ;36 p r i n t m a t ( "B =" , B , LDB, NRHS ) ;37
38 d g e s v (&N, &NRHS, A, &LDA, IPIV , B , &LDB, &INFO ) ;39 i f ( INFO == 0) {40 c o p y m a t t (B , X, NRHS, LDB ) ;41 p r i n t m a t ( "X =" , X, LDB, NRHS ) ;42 } e l s e {43 p r i n t f ( "Failure\n" ) ;44 }45 p r o d u c t m a t ( Ai , X, B , N, N, N ) ;46 p r i n t m a t z ( "AX =" , B , N, N ) ;47
48 f c l o s e ( f i n ) ;49 f r e e (A ) ; f r e e ( Ai ) ; f r e e (B ) ; f r e e (X ) ; f r e e ( IPIV ) ;50
51 re turn 0 ;52 }
目次へ15
Cプログラミング 4 連立方程式の解法と行列
47 行目,マシンエプシロン ε(DBL EPSILON として
float.h に定義されている)よりも小さな値は 0 と表示
する関数 print mat z を使って AX の検算結果を綺麗に
(誤魔化して)表示させています.
実行例の画面を示します.
$ ./inv_lapack matrix2.dat
>>> Using LAPACK dgesv_ <<<
A =
10 -2 -1 1
-2 10 1 1
-1 1 10 -2
1 1 -2 -10
B =
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
X =
0.104 0.0187 0.0105 0.0101
0.0187 0.103 -0.00577 0.0133
0.0105 -0.00577 0.0978 -0.0191
0.0101 0.0133 -0.0191 -0.0938
AX =
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
目次へ16
Cプログラミング 5 多項式の評価
5 多項式の評価
5.1 ホーナー法
以下のような多項式の値を効率的に計算する方法を考え
ましょう.
p(x) = anxn + an−1xn−1 + · · · + a1x + a0 =
n∑
k=0
ak xk (15)
まず,何も工夫せずに式のまま単純に表わすとすれば,次
のようになります.
s = 0;
for (k = n; k >= 0; k--) {t = 1;
for (j = 1; j <= k; j++) {t *= x;
}s += a[k]*t;
}
ここで a[k]は xkの係数を格納している配列です.この計
算法では乗算がn
∑
k=1
k =n(n + 1)
2回,加算が n回行われま
す.これに対して,以下のような計算法では乗算・加算が
ともに n回しか行われません.
p(x) = (· · · (anx + an−1)x + an−2)x + · · · + a1)x + a0 (16)
具体的なコードは,
s = a[k];
for (k = n-1; k >=0; k--) {s = s*x + a[k];
}
であり,この評価法はホーナー法と呼ばれます.ホーナー
法の計算回数は単純な方法の4
n倍ですから,nが大きい場
合には劇的に実行時間が改善されます.
なお,数学関数ライブラリに含まれる pow(x,y)を使え
ば,次のように計算回数が見掛け上ホーナー法と等しい手
順も考えられます.ところが,関数の呼出に時間がかかる
ため,実行速度はあまり速くありません.
s = 0;
for (k = 0; k <= n; k++) {s += a[k]*pow(x,k);
}
例題11 次数を大きく n = 1000として,多項式の値を単
純な方法とホーナー法で計算し,実行時間を比べなさい.
[解] 近年パソコンの CPUの演算性能は高くなってきてお
り,加減乗除の計算を 109回程度するのに 1秒程度しかか
からなくなっています.そこで,式の値の計算を 1000回
繰り返すことでリニアに時間を伸ばし,さらに µ秒まで計
測可能なタイマー関数 gettimeofdayを利用して実行時間
を計っています.
リスト22 horner.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < s y s / t ime . h>5
6 # d e f i n e N 10007
8 double d i f f t i m e m s e c ( s t r u c t t i m e v a l s , s t r u c t t i m e v a l f )9 {
10 re turn 1000 ∗ ( f . t v s e c − s . t v s e c )11 + 0 . 001 ∗ ( double ) ( f . t v u s e c − s . t v u s e c ) ;12 }13
14 double h o r n e r ( double x , double ∗a , i n t n )15 {16 i n t k ;17 double s ;18
19 s = a [ n ] ;20 f o r ( k = n − 1 ; k >= 0 ; k−−) {21 s = s ∗ x + a [ k ] ;22 }23
24 re turn s ;25 }26
27 double s i m p l e ( double x , double ∗a , i n t n )28 {29 i n t i , k ;30 double s , t ;31
32 s = 0 ;33 f o r ( k = n ; k >= 0 ; k−−) {34 t = 1 . 0 ;35 f o r ( i = 1 ; i <= k ; i ++) {36 t ∗= x ;37 }38 s += a [ k ] ∗ t ;39 }40
41 re turn s ;42 }43
44 i n t main ( i n t argc , char ∗ a r gv [ ] )45 {46 i n t i , j ;47 double s , x = 1 . 0 , a [N + 1 ] , d t ;48 s t r u c t t i m e v a l t s , t f ;49
50 i f ( a r g c >= 2) {51 x = a t o f ( a r gv [ 1 ] ) ;52 }53 f o r ( i = 0 ; i < N + 1 ; i ++)54 a [ i ] = 1 . 0 ;55
56 g e t t i m e o f d a y (& t s , NULL ) ;57 f o r ( j = 0 ; j < N; j ++) {58 s = s i m p l e ( x , a , N ) ;59 }60 g e t t i m e o f d a y (& t f , NULL ) ;61 d t = d i f f t i m e m s e c ( t s , t f ) ;62 p r i n t f ( "Simple: %.3f [ms]\n"63 " p(%.f) = %.15g\n" , d t , x , s ) ;64
65 g e t t i m e o f d a y (& t s , NULL ) ;66 f o r ( j = 0 ; j < N; j ++) {67 s = h o r n e r ( x , a , N ) ;68 }69 g e t t i m e o f d a y (& t f , NULL ) ;70 d t = d i f f t i m e m s e c ( t s , t f ) ;71 p r i n t f ( "Horner: %.3f [ms]\n"72 " p(%.f) = %.15g\n" , d t , x , s ) ;73
74 re turn 0 ;75 }
コンパイルのためのコマンドと,実行例を以下に示しま
目次へ17
Cプログラミング 5 多項式の評価
す.1000/4=250倍以上の差が出ています.� �
$ gcc -o horner horner.c -lm -Wall��
��
� �
$ ./horner 0.9
Simple: 2513.412 [ms]
p(1) = 10
Horner: 6.364 [ms]
p(1) = 9.99999999999999
問題 5 多項式の値を数学関数ライブラリにある冪乗関数
pow(x,y)を用いて計算する方法の実行時間を,ホーナー
法と比較するプログラムを作成しなさい.
問題 6 例題の horner.cは,多項式 p(x)の独立変数 xの
値を実行時に与えて変化させる仕様ですが,多項式の次数
nを実行時に与えられるように改変しなさい.
5.2 多項式による関数の近似
5.2.1 関数のテーラー展開
多くの初等関数はテーラー展開が可能で,有限項で打ち
切った場合には関数の多項式近似となります.関数 f (x)が
滑らかならば x = aにおけるテイラー展開は,
f (x) =
∞∑
k=0
f (n)(a)
k!(x − a)n (17)
であり,有限項 nで打ち切った近似多項式
pn(x) =
n∑
k=0
f (n)(a)
k!(x − a)n (18)
の誤差は,ξを xと aの間の点として,
f (x) − pn(x) =f (n+1)(ξ)
(n + 1)!(x − a)(n+1) (19)
で与えられることは,微積分学の教える所です.
テーラー展開は,理論的な誤差評価式も与えられており,
最も基礎的で重要な近似計算法です.ところで,テーラー
展開に基づく多項式の近似精度を高くするためには,打ち
切りの次数 nを増加させればよいことは明らかです.しか
しながら,実際には収束が進まず,そのままの形では満足
のゆく速度が得られない場合があります.
例題12 次のテーラー展開に基づく多項式で指数関数を
近似し,誤差を評価しなさい.
exp(x) = 1 + x +1
2!x2 +
1
3!x3 + · · · =
∞∑
k=1
xk
k!
[解] 近似式の項数 (≥ 5),理論的に与えられる最大誤差
(見積誤差),実際の誤差(数学関数 exp(x)が真の値を返
している仮定して)を出力する解答例,及び x = 2とした
ときの実行画面の例を以下に示します.
リスト23 exp err.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4
5 double exp ( double x , i n t n , double ∗ e )6 {7 i n t k ;8 double s , a [ n+1 ] ;9
10 a [ 0 ] = 1 ;11 f o r ( k = 1 ; k <= n ; k++) {12 a [ k ]= a [ k − 1 ] / ( double ) k ;13 }14 s = a [ n ] ;15 f o r ( k = n−1; k >= 0 ; k−−) {16 s = s ∗ x + a [ k ] ;17 }18 ∗ e = exp ( x )∗ pow ( x , n+1)∗ a [ n ] / ( n+1 ) ;19
20 re turn s ;21 }22
23 i n t main ( i n t argc , char ∗ a r gv [ ] )24 {25 double r e r r , merr ;26 double x = a t o f ( a r gv [ 1 ] ) ;27 i n t imax = a t o i ( a r gv [ 2 ] ) , i ;28
29 p r i n t f ( "# x = %f\n" , x ) ;30 f o r ( i = 5 ; i <=imax ; i ++) {31 r e r r = exp ( x ) − exp ( x , i ,& merr ) ;32 p r i n t f ( "%d\t%.15e\t%.15e\n" , i , r e r r , merr ) ;33 }34
35 re turn 0 ;36 }
$ ./exp_err 2 20
# x = 2.000000
5 1.223894322639838e-01 6.568049865716133e-01
6 3.350054337509523e-02 1.876585675918895e-01
7 8.103717978269920e-03 4.691464189797238e-02
...
18 4.786393503763975e-12 3.184668705090635e-11
19 4.760636329592671e-13 3.184668705090636e-12
20 4.529709940470639e-14 3.033017814372034e-13
誤差が減少していく様子の方対数プロットを以下に示し
ます.項数 22で,誤差がなくなってしまいました.
1e-25
1e-20
1e-15
1e-10
1e-05
1
5 10 15 20 25 30
項数
誤差最大誤差
問題 7 0 ≤ x ≤ 5について,例題の指数関数の近似式の
誤差が 1 × 10−12 以下になる項数を求めるプログラムを作
成しなさい.
目次へ18
Cプログラミング 5 多項式の評価
〈ヒント〉結果のプロットを以下に示します.
0
5
10
15
20
25
30
0 1 2 3 4 5
項数
x
問題 8 対数関数のテーラー展開
log(1 + x) = x − 1
2x2 +
1
3x3 − · · · = −
∞∑
k=1
(−1)k
k
は交代級数であるため,収束が遅い.そこで,以下のよう
な無限級数展開に基づき,近似関数を作成しなさい.
log1 + x
1 − x= 2
(
x +1
3x3 +
1
5x5 + · · ·
)
= 2
∞∑
k=0
x2k+1
2k + 1
問題 9 以下に示す第 1種ベッセル関数 Jn(x)の無限級数
展開を基にして J0(x)の近似関数を作成しなさい.
Jn(x) =(
x
2
)n ∞∑
k=0
(−1)k
(
x
2
)2k
k!Γ(n + k + 1)
�0 次と 1 次および第 n 次の第 1 種ベッセル関数は j0(x), j1(x),
jn(n,x) という名前で,また第 2 種ベッセル関数は y0(x), y1(x),
yn(n,x) という名前で数学関数に含まれています.
+参考書・ウェブサイト:数値計算プログラム
• Ooura’s Mathematical Software Package
• 浜田穂積:近似式のプログラミング(培風館,1995)
5.3 コンパイラーの最適化オプション
多項式の値を求めるアルゴリズムとしてホーナー法は優
れた方法です.計算回数が必要最小限であることから,実
行時間が少なくて済むことを確認しました.実は誤差も少
ないのですが,そのことを厳密に確認するのはなかなか厄
介ですので,講義の概要のところであげた参考書で自己学
習してください.
実行時間を少なくするには,アルゴリズムを改善するだ
けではなく,パソコンに搭載されている CPUの演算機能
を活用するように指示することでも達成できます.しかし,
CPUというハードウェアの知識を要求されるので,どのよ
うな指示をすべきかについて一概に論ずることはできませ
ん.一方,実数演算に限っていえば,専用の数値演算プロ
セッサーが大きな役割を果していることははっきりしてい
ます.また,現在では動画関連の処理を行う画像プロセッ
サー GPUの開発が盛んであることは皆さん御存じと思い
ます.いずれにしても,その機能を最大限に活用するには
相当の知識が要求されますから,そこで,なかば諦めつつ
コンパイラーに任せてしまうことにしましょう.
�数値演算プロセッサー
パソコンの CPU には,浮動小数点数の演算や数学関数のうちの一部の
関数 sin(x), cos(x),√
x の値を高速に演算する数値演算プロセッサーが搭
載されているのが一般的になっています.コードの見直しによる最適化
(純粋なソフトウェア最適化)は,これらのいわばハードウェアによる効
率化にはとうてい敵いません.また,行列の演算もサポートしている場
合があり,最適化の内容は大変複雑になっています.したがって,CPU
を設計・製造している企業自身が商品化しているコンパイラーに比べて,
限られた設計・製造情報に基づいてコード化せざるを得ない GNUのコン
パイラー GCCは,残念ながら最適化オプションが完全とはいえず,CPU
の能力を最大限に引き出せないのが現状です.
5.3.1 取り敢えず - O2
man gccにより最適化のオプションは-Olevelであること
がわかります.レベル 3が最高ですが,場合によってはや
りすぎることもあるそうなので,無難な- O2を指定してコ
ンパイルをやり直し,実行時間を比較してみましょう.
例題13 多項式の値を計算する方法として単純な方法と
ホーナー法を用い,両者の実行時間を比較するプログラム
を最適化オプションを変化させてコンパイルし,それぞれ
の実行時間を比較しなさい.
[解] 実行画面例を示します.このプログラムでは,最適化
のレベルの違いによる実行時間の変化はありませんでした.
$ gcc -o horner horner.c -lm -O0
$ ./horner
Simple: 2464.573 [ms]
p(1) = 1001
Horner: 6.340 [ms]
p(1) = 1001
$ gcc -o horner horner.c -lm -O2
$ ./horner
Simple: 752.635 [ms]
p(1) = 1001
Horner: 2.977 [ms]
p(1) = 1001
$ gcc -o horner horner.c -lm -O3
$ ./horner
Simple: 756.367 [ms]
p(1) = 1001
Horner: 2.979 [ms]
p(1) = 1001
目次へ19
Cプログラミング 5 多項式の評価
5.4 誤差
計算機による計算は有限桁の数を用いた有限回数の手順
と言う制限があるため,本質的に近似値を得るに過ぎませ
ん.有限回で計算を打ち切る(テーラー展開による多項式
近似など)ことどよる誤差は打ち切り誤差と呼ばれます.
また,無限桁の実数を有限桁に収めるために発生する誤
差は丸め誤差と呼ばれます.一般に,いつ打ち切るかにつ
いては指示しなければならないので,その意味で打ち切り
誤差については前もって検討している場合が多いといえま
しょう.それに比して,丸め誤差は思わぬところで発生す
る場合があり注意しなければなりません.
5.4.1 二次方程式の解の公式
二次方程式が 2実根を持つ場合の解の公式は以下のよう
なものです.
x2 + bx + c = 0
x± =−b ±
√b2 − 4c
2
解の公式そのままに素直に計算すると係数 b, cが b2 ≫ c
の関係にある場合,b < 0ならば解 x−は僅かな差の数同士
の引き算となってしまいます.その結果,解 x+ に比べて
有効桁の少ない結果しか得られません.この場合には,2
根の関係 x+x− = cより
x− =c
x+
を用いれば桁落ちを防ぐことができます.
例題14 x2 − x+ 1.0× 10−n = 0 (n:正整数)の 2実根うち
小さい方の根を,根の公式そのままで計算した結果と根の
公式を用いて計算した結果を比較しなさい.
[解] n = 1, · · · , 15の場合の解答例と実行画面を示します.
リスト24 quadratic.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4
5 i n t
6 main ( void )7 {8 i n t n ;9 double c , xp , xm ;
10
11 p r i n t f ( "c= \t xm= \t\t\tc/xp=\n" ) ;12 f o r ( n = 1 ; n < 16 ; n++) {13 c = pow(10 , −n ) ;14 xp = (1+ s q r t (1−4∗ c ) ) / 2 ;15 xm = (1− s q r t (1−4∗ c ) ) / 2 ;16 p r i n t f ( "%.1E\t %.15g\t%.15g\n" , c , xm , c / xp ) ;17 }18
19 re turn 0 ;20 }
$ ./quadratic
c= xm= c/xp=
1.0E-01 0.112701665379258 0.112701665379258
1.0E-02 0.0101020514433644 0.0101020514433644
1.0E-03 0.00100100200501402 0.00100100200501404
1.0E-04 0.000100010002000495 0.0001000100020005
1.0E-05 1.00001000020167e-05 1.00001000020001e-05
1.0E-06 1.00000100000663e-06 1.000001000002e-06
1.0E-07 1.00000009994883e-07 1.00000010000002e-07
1.0E-08 1.00000001057587e-08 1.00000001e-08
1.0E-09 1.00000002722922e-09 1.000000001e-09
1.0E-10 1.00000008274037e-10 1.0000000001e-10
1.0E-11 1.00000008274037e-11 1.00000000001e-11
1.0E-12 9.99977878279878e-13 1.000000000001e-12
1.0E-13 1.00031094518727e-13 1.0000000000001e-13
1.0E-14 9.99200722162641e-15 1.00000000000001e-14
1.0E-15 9.99200722162641e-16 1e-15
目次へ20
Cプログラミング 6 方程式の解:NEWTON法
6 方程式の解:Newton法
次の方程式を解く方法を考えましょう.
f (x) = 0 (20)
代数方程式であっても,解の公式は 3次までのものを学習
したことがある程度ですぐに手計算できるのは 2次までで
す.まして,sin(x), ex などの超越関数を含む方程式では,
特別な場合を除いて数値的に求める他ないのです.しかし,
闇雲に探索を開始することはできないので,まずは比較的
広い範囲で曲線 y = f (x)を描き,x軸と交わる点のおおよ
その位置を把握することが肝心です.
6.1 数値解とは
数値的に求めるとは,真の値を x∗ としたときに,算出
された値 xk(一般に,ある手順により近似値を改善してい
くので,k回の繰り返しで得られた値を xkとおく)との誤
差 e = xk − x∗ の絶対値
|e| = |xk − x∗| (21)
が許容誤差範囲 εよりも小さくなった場合に,すなわち,
|xk − x∗| < ε (22)
を満たした時点で計算を打ち切り,xkを解(改めて xcalと
おく)とするものです.したがってもちろん,真の値は
xcal − ε < x∗ < xcal + ε (23)
の範囲にあることになります.ところが,良く考えてみる
と真の値 x∗ が判ってませんから,誤差 e = xk − x∗ を直接
的には評価できません.そこで,誤差の上限が評価できる
ような計算方法が考案されてきました.
6.2 2分法 (Bisection)
f (x)の符号は真の値を越えるときに反転しますから,x∗
の両側に適当な 2つの値 xL, xU を選べば,
xL < x∗ < xU , f (xL) f (xU) < 0 (24)
となっているはずです.この両側の値 xL, xU の差を条件
(24)を保持しながら狭めていって,ある値 δより小さくで
きたとしましょう.すると,
max{
|xU − x∗|, |xL − x∗|}
< |xU − xL| < δ (25)
が成立しますから,明らかに xLおよび xU の真値に対する
残差は δより小さくなっています.また,この方法は必ず
解を得ることができることは,手順より明らかでしょう.
具体的な判定のアルゴリズムは, f (xL) f (xU) < 0を保ち
ながら狭めていけばよいので,epsを打ち切りの指標とし
て以下のようになります.
do {dx = xU - xL;
x = (xU + xL)/2;
if (f(x)*f(xU) > 0) xU = x; else xL = x;
} while (dx > eps);
区間を 1/2の幅の 2つの部分に分けて狭めていくので 2分
法と呼ばれます.従って,最初の幅を δ0とすると,k回の
繰り返し後の区間の幅(=誤差の上限)δは,
δ =
(
1
2
)k
δ0 (26)
で与えられます.
xU
0
xL
0
xU
1xL
1
xU
2
xL
3xU
3
xL
2
図 4 2分法による xL, xU の求め方
例題15 2分法の最初の区間幅 δ0が 1である場合に,区間
幅 δが 1 × 10−15 より小さくなるのは何回繰り返した後か.
[解]
(
1
2
)k
< 1 × 10−15
を解けばよい.両辺の対数をとれば
k log(0.5) < −15 log(10)
∴ k > −15 log(10)/ log(0.5) = 49.83
であるから,50回.
例題16 2分法を用いて,超越方程式
1
2tan x − x = 0
(
0 < x <π
2
)
の解を求めなさい.
目次へ21
Cプログラミング 6 方程式の解:NEWTON法
-1
-0.5
0
0.5
1
0 0.2 0.4 0.6 0.8 1 1.2 1.4
x
tan(x)/2 - x
図 5 y = f (x) =1
2tan(x) − xのプロット
[解] 解答例を以下に示します.初期の区間の端の値 xL, xU
を入力する仕様になっています.また,
リスト25 bisection.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <float.h>
5
6 #define N 20
7
8 double f(double x) {
9 return tan(x)/2 - x ;
10 }
11
12 int
13 main(int argc, char *argv[])
14 {
15 int i = 1;
16 double x, xL, xU, dx;
17
18 if (argc < 3) {
19 printf("Usage %s xL xU\n", argv[0]);
20 exit(0);
21 }
22 xL = atof(argv[1]);
23 xU = atof(argv[2]);
24
25 printf("# I\txk\t\t\tdx\n");
26 printf("#----------------------"
27 "----------------------\n");
28 do {
29 dx = xU - xL;
30 x = (xU + xL)/2.0;
31 if (f(x)*f(xU) > 0){
32 xU = x;
33 } else {
34 xL = x;
35 }
36 printf("%03d\t%+.15e\t%+.6e\n", i, x, dx);
37 i++;
38 } while (dx > DBL_EPSILON );
39
40 return 0;
41 }
実行画面例を示します.
# I xk dx
#--------------------------------------------
001 +1.100000000000000e+00 +2.000000e-01
002 +1.150000000000000e+00 +1.000000e-01
003 +1.175000000000000e+00 +5.000000e-02
004 +1.162500000000000e+00 +2.500000e-02
005 +1.168750000000000e+00 +1.250000e-02
...
050 +1.165561185207211e+00 +4.440892e-16
051 +1.165561185207211e+00 +2.220446e-16
問題10 2分法を用いて,次の超越方程式の解を求めなさ
い.複数の解がありそうな場合には,0に近い領域の解を
探しなさい.
( i ) x − cos x = 0
( ii ) x2 − e−x = 0
(iii) x2 − e−x cos 2x = 0
(iv) 1 − e−x
x + 1− e−x
x − 1= 0
6.3 はさみうち法 (False position)
2分法では関数の性質に全く無関係に,誤差上限(=区間
幅)が 2の冪乗で小さくなっていくので,最初の区間幅を
1とすると,誤差を 10−15より小さくするには 50回程度の
繰り返しが必要でした.この回数を少しでも少なくする改
善されたアルゴリズムの一つにはさみうち法があります.
図6を見れば判るように関数上の 2点を結ぶ直線と x軸と
の交点を新たな区間の端に用いるというものです. f (x)の
値を利用するので f (x)の性質によっては,少ない繰り返
しで誤差上限を狭めることができます.
x0
x1x2
x3xk+1xk
y = f (x)
図 6 はさみうち法による xk (k = 1, 2, · · · )の解の求め方
新しい区間の端,すなわち直線と x軸の交点を求める計
算式は以下のように与えられます.
xk+1 = xk −x0 − xk
f (x0) − f (xk)f (xk) =
xk f (x0) − x0 f (xk)
f (x0) − f (xk)(27)
なお, f (x0)と f (xk+1)の符号が同じなってしまった場合に
は,x0を xk の位置に変更して,真値 x∗をはさむような区
間にします.
目次へ22
Cプログラミング 6 方程式の解:NEWTON法
主要部の具体的なコーディングは以下のようなものです.
xp,xが xk, xk+1を表しています.区間幅の絶対値 |xk+1− xk |と関数の絶対値 | f (xk+1)|のどちらかが十分小さくなったことを終了条件にしています.
f0 = f(x0);
fp = f(xp);
x = (xp*f0 - x0*fp)/(f0 - fp);
fx = f(x);
if ( fabs(x-xp) < DBL EPSILON
|| fabs(fx) < DBL EPSILON ) break;
if ( fx*f0 > 0 ) x0 = xp;
xp = x;
例題17 はさみうち法を用いて,超越方程式
1
2tan x − x = 0
(
0 < x <π
2
)
の解を求めなさい.
[解] 解答例を以下に示します.2分法と同じく,探索区間
の端の値 xL, xU の初期値を入力する仕様になっています.
リスト26 falseposition.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <float.h>
5
6 #define N 100
7
8 double f(double x) {
9 return tan(x)/2 - x ;
10 }
11
12 int
13 main(int argc, char *argv[])
14 {
15 int k = 0;
16 double x, xp, x0, fx, fp, f0;
17
18 if (argc < 3) {
19 printf("Usage %s xL xU\n", argv[0]);
20 exit(0);
21 }
22 xp = x = atof(argv[1]);
23 x0 = atof(argv[2]);
24
25 printf("# k\txk\t\t\tf(xk)\n");
26 printf("#-----------------------"
27 "---------------------\n");
28 for(k = 1; k < N; k++) {
29 f0 = f(x0);
30 fp = f(xp);
31 x = (xp*f0 - x0*fp)/(f0 - fp);
32 fx = f(x);
33 printf("%03d\t%+.15e\t%+.6e\n", k, x, fx);
34 if ( fabs(x-xp) < DBL_EPSILON
35 || fabs(fx) < DBL_EPSILON ) break;
36 if ( fx*f0 > 0 ) x0 = xp;
37 xp = x;
38 }
39
40 return 0;
41 }
6.3.1 修正はさみうち法
はさみうち法はある定点から直線を引いて x軸との交点
を求める方法ですから,真値 x∗ の付近では傾きが大きく
なり(x軸方向への移動が少なくなる),収束が遅くなる
ことがあります.そこで,強制的に傾きを小さくすること
で,この遅延を改善する方法が考案されました.つまり直
線を引く定点の関数値を 1/2に減少させていきます.
f0 ←1
2f0 (28)
なお,定点の変更があった場合には f0 = f (xk)と値を取り
直します.
x0
x1x2
x3
x4
y = f (x)
f (x0)12
f (x0)12
2
図 7 修正はさみうち法による xk (k = 1, 2, · · · )の解の求め方
例題18 修正はさみうち法を用いて,超越方程式
1
2tan x − x = 0
(
0 < x <π
2
)
の解を求めなさい.
[解] 解答例を示します.はさみうち法のソースと比較し
て,ほんの僅かな修正で済んでいることが判ります.
リスト27 modified fp.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < f l o a t . h>5
6 # d e f i n e N 1007
8 double f ( double x ) {9 re turn t a n ( x ) / 2 − x ;
10 }11
12 i n t
13 main ( i n t argc , char ∗ a r gv [ ] )14 {15 i n t k ;16 double x , xp , x0 , fx , fp , f0 ;17
18 i f ( a r g c < 3) {19 p r i n t f ( "Usage %s xL xU\n" , a r gv [ 0 ] ) ;20 e x i t ( 0 ) ;21 }22 x = a t o f ( a r gv [ 1 ] ) ;23 x0 = a t o f ( a r gv [ 2 ] ) ;24
25 f0 = f ( x0 ) ;26 p r i n t f ( "# k\txk\t\t\tf(xk)\n" ) ;
目次へ23
Cプログラミング 6 方程式の解:NEWTON法
27 p r i n t f ( "#----------------------"28 "----------------------\n" ) ;29 f o r ( k = 1 ; k < N; k++) {30 xp = x ;31 fp = f ( xp ) ;32 x = ( xp∗ f0 − x0∗ fp ) / ( f0 − fp ) ;33 fx = f ( x ) ;34 p r i n t f ( "%03d\t%+.15e\t%+.6e\n" , k , x , fx ) ;35 i f ( f a b s (1− fx / fp ) < DBL EPSILON36 | | f a b s ( fx ) < DBL EPSILON ) break ;37 i f ( fx ∗ f0 > 0 ) {38 x0 = xp ;39 f0 = f ( x0 ) ;40 } e l s e f0 = f0 / 2 ;41 }42
43 re turn 0 ;44 }
6.4 Newton法
f ’(xk) =
xkxk+1
f (x)
xk- xk+1
f (xk)
y = f(x)
xk+2
図 8 Newton法による xk (k = 1, 2, · · · )の求め方
f (x)が 1階微分可能な場合に有力な方法として Newton
法があります.図のように,xkにおける y = f (x)の接線と
x軸との交点 xk+1を求めます.続いて,xk+1における接線
と x軸の交点 xk+2を求めます.以下同様にして得られる数
列 xk は,以下のような漸化式で表されます.
xk+1 = xk −f (xk)
f ′(xk)(29)
すると,この数列 xk は,ある条件下で f (x) = 0の解に収
束することが証明できます.すなわち,誤差 Rk = xk − x∗
について,
ek+1 = xk+1 − x∗ = xk −f (xk)
f ′(xk)− x∗
= ek −f (x∗ + ek)
f ′(x∗ + ek)
≃ ek −f (x∗) + f ′(x∗)ek +
12
f ′′(x∗)e2k
f ′(x∗) + f ′′(x∗)ek
=e2
kf ′′(x∗) − 1
2f ′′(x∗)e2
k
f ′(x∗) + f ′′(x∗)ek
≃ f ′′(x∗)
2 f ′(x∗)e2
k (30)
が成立しますから, f ′(x∗) , 0,すなわち x∗ が重根である
場合を除いて,α2 = f ′′(x∗)/2 f ′(x∗)とおいて,
ek+1 ≃ α2e2k ≃ α
4e4k−1 ≃ · · · ≃ (αe0)2(k+1) (31)
がほぼ成立します.従って,真の値 x∗ に十分近い初期値
x0 を選べば初期の誤差 e0 = x0 − x∗ < α−1 とすることがで
き,αe0の 2乗に比例して残差が減少することになります.
このような収束を 2次収束と呼びます.
なお,誤差 ek の上限を計算しているわけではなく,打
ち切りの判定は,|xk+1 − xk |がある値以下になったかで行います.
ε > |xk+1 − xk | ≃ |ek ||(αe0)2 − 1| (32)
∴ε
|(αe0)2 − 1|& |ek | (33)
従って,初期値が十分に真値に近い場合には |(αe0)2−1| ≃ 1
ですから,誤差の上限も ε未満といえます.
具体的な判定のアルゴリズムは,収束しない場合もある
ので,繰り返しの数の上限を定めておき,無限ループにな
らない工夫を盛り込んで,以下のようになります.
for (k = 0; k < N; k++) {dx = -f(x)/df(x);
x += dx;
if (fabs(dx) <= eps) {printf("x = %.12e", x);
}printf("Can’t solve\n");
⇐繰り返しの上限:N
⇐ df(x)は微分値を返す関数
⇐成功:結果の表示
⇐失敗の旨の表示
例題19 Newton法を用いて f (x) = tan(x)/2 − x = 0の解
を求めるプログラムを作成しなさい.実行時に初期値を入
力する仕様としなさい.
[解] リスト28 newton.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <float.h>
5
6 #define N 20
7
8 double f(double x) {
9 return tan(x)/2 - x;
10 }
11
12 double df(double x) {
13 return 1/(2*cos(x)*cos(x)) - 1;
14 }
15
16 int
17 main(int argc, char *argv[])
18 {
19 int k;
20 double x, dx;
21
22 if (argc < 2) {
23 printf("Usgae: %s x0\n", argv[0]);
目次へ24
Cプログラミング 6 方程式の解:NEWTON法
24 exit(0);
25 }
26 x = atof(argv[1]);
27
28 printf("# k\txk\t\t\tf(xk)\n");
29 printf("#-----------------------"
30 "---------------------\n");
31
32 for (k = 1; k < N; k++) {
33 dx = -f(x)/df(x);
34 x += dx;
35 printf("%03d\t%+.15e\t%+.6e\n", k, x, f(x));
36 if ( fabs(dx) <= DBL_EPSILON ){
37 printf("#%2d: Solved!\n", k);
38 return 0;
39 }
40 }
41 printf("#Can’t solve\n");
42
43 return 0;
44 }
適切な初期値を設定した場合の実行例を示します.たっ
た 5回の繰り返しで解に到達しました.
$ ./newton 1.2
# k xk f(xk)
#--------------------------------------------
001 +1.169346024452516e+00 +8.499682e-03
002 +1.165609311070920e+00 +1.067156e-04
003 +1.165561193040886e+00 +1.736777e-08
004 +1.165561185207211e+00 +2.383076e-16
005 +1.165561185207211e+00 +2.383076e-16
# 5: Solved!
初期値の値が適切でなく,解が得られない場合の実行例
を示します.
$ ./newton 0.9
# k xk f(xk)
#--------------------------------------------
001 +1.818100247354237e+00 -3.798518e+00
002 +2.335320113296923e+00 -2.856643e+00
003 +6.792318000583157e+01 -6.917889e+01
...
018 -1.012617109752505e+07 +1.012617e+07
019 -1.483916248510458e+07 +1.483916e+07
Can’t solve
6.4.1 微分値の数値計算
前節の Newton法で関数の微分値は,1次導関数を解析
的に求めたものを使って計算しました.初等関数の微分は
積分とは異なり,必ず求めることが可能ですが,複雑な場
合にはそれなりの手間がかかります.そこで,微分値を数
値計算により求めることにします.最も単純な方法は以下
のようなものです.
f ′(x) ≃ f (x + ∆x) − f (x)
∆x(34)
例題20 前節の Newton法において関数値/微分値はそれ
ぞれ解析的な関数形を求めて計算しましたが,その微分値
のみを (34)に従って数値計算するプログラムに改造しな
さい.
[解] 問題となるのは,微小量 ∆xですが,xk+1 − xkの半分
としてみます.
リスト29 newton auto.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <float.h>
5
6 #define N 20
7
8 double f(double x)
9 {
10 return x - tan(x)/2;
11 }
12
13 double fdf(double (*f)(), double x, double dx)
14 {
15 return f(x)*dx/(f(x+dx)-f(x));
16 }
17
18 int
19 main(int argc, char *argv[])
20 {
21 int k = 0;
22 double x, dx, diff_x;
23
24 if (argc < 3){
25 printf("Usage %s x diff_x\n",argv[0]);
26 exit(0);
27 }
28
29 x = atof(argv[1]);
30 diff_x = atof(argv[2]);
31
32 printf("# k\txk\t\t\tf(xk)\n");
33 printf("#-----------------------"
34 "---------------------\n");
35
36 for (k = 0; k < N; k++) {
37 dx = -fdf(f, x, diff_x);
38 x += dx;
39 printf("%03d\t%+.15e\t%+.6e\n", k, x, f(x));
40 if ( fabs(dx) <= 2*DBL_EPSILON ){
41 printf("%2d: Solved!\n",k);
42 return 0;
43 }
44 diff_x = dx/2; /* 微分計算に使う微小変化 */
45 }
46 printf("Can’t solve\n");
47
48 return 0;
49 }
6.5 もっと収束の速い方法
漸化式(反復関数と呼ばれる)を工夫すると,もっと高
い次数で収束することが可能です.例えば
xk+1 = x − f (xk)
f ′(xk)
(
1 +f (xk) f ′′(xk)
2 f ′(xk)2
)
(35)
と定義する Householder法や,
xk+1 = x − 2 f (xk) f ′(xk)
2 f ′(xk)2 − f (xk) f ′′(xk)(36)
目次へ25
Cプログラミング 6 方程式の解:NEWTON法
と定義するHalley法は,3次の収束となることが知られて
います.
問題11 Householder法もしくはHalley法を用いて x2−2 =
0の解を求め,同じ打ち切り誤差内の解が得られる反復回
数を,Newton法と比較してみなさい.
11の解 反復の方法(NewtonまたはHalley)をオプショ
ン引数にとって起動するプログラム例を示します.
リスト30 q11.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < f l o a t . h>5 # i n c l u d e < s t r i n g . h>6
7 # d e f i n e N 108
9 double newton ( double x )10 {11 re turn ( x∗x −2 ) / ( 2 ∗ x ) ;12 }13
14 double h a l l e y ( double x )15 {16 re turn (4∗ x ∗ ( x∗x − 2 ) ) / ( 8 ∗ x∗x − 2∗ ( x∗x − 2 ) ) ;17 }18
19 i n t main ( i n t argc , char ∗ a r gv [ ] )20 {21 char func , fname [ 1 2 ] ;22 i n t i ;23 double x , dx , (∗ r f ) ( ) ;24
25 i f ( a r g c < 3) {26 p r i n t f ( "Usgae: %s x0 method(N or H)\n" , a r gv [ 0 ] ) ;27 e x i t ( 0 ) ;28 }29 x = a t o f ( a r gv [ 1 ] ) ;30 f unc = a r gv [ 2 ] [ 0 ] ;31
32 swi t c h ( f unc ) {33 c ase ’N’ : r f=newton ; s t r c p y ( fname , "Newton" ) ; break ;34 c ase ’H’ :35 d e f a u l t : r f=h a l l e y ; s t r c p y ( fname , "Halley" ) ; break ;36 }37 f o r ( i = 0 ; i < N; i ++) {38 dx = − r f ( x ) ;39 x += dx ;40 p r i n t f ( "%2d: x=%+.15e "
41 "f(x)=%+.6e\n" , i , x , x∗x −2 ) ;42 i f ( f a b s ( dx ) <= DBL EPSILON ) {43 p r i n t f ( "%2d: Solved by %s\n" , i , fname ) ;44 re turn 0 ;45 }46 }47 p r i n t f ( "Can’t solve\n" ) ;48
49 re turn 0 ;50 }
6.6 収束の比較
収束の速度(解を得るまでの繰り返しの回数)を比較し
てみましょう.この点に関して Newton法は大変優れてい
ることが判ります.
10-14
10-12
10-10
10-8
10-6
10-4
10-2
100
0 10 20 30 40 50
絶対誤差
繰り返し回数
NewtonMFP
FPBS
図 9 非線形方程式 f (x) = tan(x)/2 − x = 0の 1.1付近の
解に対する,2 分法,はさみうち法,修正はさみうち法,
newton 法の収束性の比較:初期値を newton 法では 1.2,
他の方法では 1と 1.2に指定した場合.
収束を比較した図9は次のような手順で作成できます.是
非,試してください.
( i ) データファイル newton.dat等をシェルの入力リダイレ
クトを用いて,� �
./newton 1.2 > newton.dat��
��
� �
のように起動して作成する.
( ii ) 以下の gnuplot(8節を参照)のスクリプトを� �
gnuplot fsolve.plt��
��
� �
とコマンドライン上で走らせる.
リスト31 fsolve.plt
1 s e t te rm p o s t eps enh c o l o r "Times-Roman ,16"2 s e t o u t "fsolve.eps"3 s e t x r a nge [ 0 : 5 0 ]4 s e t l o g s c a l e y5 s e t g r i d6 x t =1.1655611852072117 s e t y r a nge [1 e −15: 1 ]8 s e t f o r m a t y "10ˆ{%T}"9 s e t x l a b e l "{/Ryumin-Light-EUC-H=20 繰り返し回数}"
10 s e t y l a b e l "{/Ryumin-Light-EUC-H=20 絶対誤差}"11
12 p l o t \13 "newton.dat" u 1 : ( abs ( $2−x t ) ) w l p t "Newton" p t 5 ,\14 "modif_fp.dat" u 1 : ( abs ( $2−x t ) ) w l p t "MFP" p t 6 ,\15 "fp.dat" u 1 : ( abs ( $2−x t ) ) w l p t "FP" p t 7 ,\16 "bisection.dat" u 1 : ( abs ( $2−x t ) ) w l p t "BS" p t 817
18 sys tem ( "ggv fsolve.eps" )
目次へ26
Cプログラミング 7 常微分方程式の解法
7 常微分方程式の解法
x(t)に関する次のような 1階常微分方程式の初期値問題
を考えます.
dx
dt= f (t, x), x(0) = x0 (37)
この解は,両辺を形式的に tで積分すると,
x(t) = x0 +
∫ t
0
f (t, x)dt (38)
と表されます.しかし,右辺の積分の核には x(t)自身が含
まれているので積分を実行することは一般に困難です.そ
こで tを微小に変化させて,xi = x(ti)までが求まっている
として,
xi+1 = xi +
∫ ti+1
ti
f (t, x)dt (39)
と表し,右辺の微小区間の積分を求める数値解法が色々と
考案されてきました.
ここでは,与えられた初期値 x0 = x(t0)から出発して,
等間隔の点
t0, t0 + h, t0 + 2h, t0 + 3h, · · ·
における解 x(t)を逐次計算する方法を取り上げます.
7.1 オイラー法
xi = x(ti)から xi+1 = x(ti+1) = x(ti + h)を求める最も素朴
な方法は,x(ti)のまわりで x(t)を Taylor展開して,2次以
上の項を無視して得られる
xi+1 = x(ti + h) = xi + hdx
dt(ti)
= xi + h f (ti, xi) (40)
です.これはオイラー法と呼ばれます.これは概念を理解
するのには最適ですが,誤差が大きく実用的ではありませ
ん.局所的打ち切り誤差は h2 に比例しています.�
x = Φ(t) の一次導関数dx
dt= f (t, x) が与えられている時の Taylor展
開係数は, f を用いた場合には以下のようになります.
dk x
dtk=
dk−1 f
dtk−1=
{
∂
∂t+
dx
dt
∂
∂x
}k−1
f =
{
∂
∂t+ f∂
∂x
}k−1
f (41)
ただし,k = 1 の項は f そのものです.
例題21 Euler法を用いて次の微分方程式の数値解を 0 ≤t ≤ 1の範囲で求めなさい.
dx
dt= −x, x(0) = 1 (42)
また,解析解 x(t) = e−t との誤差を検討しなさい.
t
x(t)
ti ti+1
h
xi xi+1
f(ti , xi)傾き
xi
hf(ti , xi)
図 10 オイラー法による逐次計算方法の概念図
[解] 解答例 euler.cを示します.刻み幅 hを引数にして
起動して,時間の最終値 tmax = 1.0まで計算を続行させま
す.オイラー法に基づく,xの差分値 h f (ti, xi)を求める関
数 euler(....)は,
関数へのポインタ (*)f()
を第 1引数とする設計にしました. euler()を呼び出す
時,第 1引数に関数名を渡します.こうすると,式 (37)の
右辺値を帰す関数を別に定めることができるようになり汎
用性が高まります.それと逆向しますが,初期値 x(0) = 1
と解析解 e−t を静的に埋め込んであります.また,実数を
繰り返し判定(27 行)に用いるのはあまり良くないので
すが,いずれも行数節約のためということで大目にみてく
ださい.
リスト32 euler.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4
5 double f ex21 ( double t , double x )6 {7 re turn −x ;8 }9
10 double
11 e u l e r ( double (∗ f ) ( ) , double t , double x , double h )12 {13 re turn h∗ f ( t , x ) ;14 }15
16 i n t main ( i n t argc , char ∗∗ a r gv )17 {18 double h , t = 0 , x ;19
20 i f ( a r g c < 2) {21 p r i n t f ( "Usage: %s h\n" , a r gv [ 0 ] ) ;22 e x i t ( 0 ) ;23 }24 h = a t o f ( a r gv [ 1 ] ) ;25 x = 1 . 0 ;26
27 f o r ( t = 0 ; t <= 1 . 0 ; t += h ) {28 p r i n t f ( "%.3f\t%+.6e\t%+.6e\n" , t , x , x−exp (− t ) ) ;29 x += e u l e r ( fex21 , t , x , h ) ;30 }31
32 re turn 0 ;33 }
h = 0.2, 0.1としたときの実行例を示します.
目次へ27
Cプログラミング 7 常微分方程式の解法
$ ./euler 0.2
0.000 +1.000000e+00 +0.000000e+00
0.200 +8.000000e-01 -1.873075e-02
0.400 +6.400000e-01 -3.032005e-02
0.600 +5.120000e-01 -3.681164e-02
0.800 +4.096000e-01 -3.972896e-02
1.000 +3.276800e-01 -4.019944e-02
$ ./euler 0.1
0.000 +1.000000e+00 +0.000000e+00
0.100 +9.000000e-01 -4.837418e-03
0.200 +8.100000e-01 -8.730753e-03
0.300 +7.290000e-01 -1.181822e-02
0.400 +6.561000e-01 -1.422005e-02
0.500 +5.904900e-01 -1.604066e-02
0.600 +5.314410e-01 -1.737064e-02
0.700 +4.782969e-01 -1.828840e-02
0.800 +4.304672e-01 -1.886175e-02
0.900 +3.874205e-01 -1.914917e-02
1.000 +3.486784e-01 -1.920100e-02
問題12 Euler法を用いて次の微分方程式の初期値問題を
解きなさい.また,tの最終値における解析解との相対誤
差が 1.0 × 10−6 であるためには,hをどこまで小さくしな
ければならないか検討しなさい.
( i )dx
dt= −3x, x(0) = 1.0, (0 ≤ t ≤ 1), x(t) = e−3t
( ii )dx
dt= −x2, x(0) = 1.0, (0 ≤ t ≤ 1), x(t) =
1
t + 1
(iii)dx
dt= e−x, x(0) = 0.0, (0 ≤ t ≤ 1), x(t) = log(t + 1)
7.2 ホイン法
オイラー法を改良した,逐次計算法
x∗i+1 = xi + h f (ti, xi) (43)
xi+1 = xi +h
2
[
f (ti, xi) + f (ti+1, x∗i+1)
]
(44)
は,ホイン法と呼ばれます.局所的打ち切り誤差は h3 に
比例しています.
t
x(t)
ti ti+1
xi
xi+1
xi+1*h
2h2
傾き f(ti+1 , xi+1)*
f(ti , xi)傾き
図 11 ホイン法による逐次計算方法の概念図
例題22 Heun法を用いて次の微分方程式の数値解を 0 ≤
x ≤ 1の範囲で求めなさい.
dx
dt= −x, x(0) = 1 (45)
また,解析解 x(t) = e−t との誤差を Euler法と比較して検
討しなさい.
[解] 解答例 heun.cを以下に示します.微分方程式を定
義する関数 dfdt(),解析解の値を返却する関数 ax() を
プロトタイプ宣言しておきます.実際の中身は odedef.c
に記述することにします.コンパイルは以下の様に実行し
ます.� �
gcc -o heun heun.c odedef.c -lm -Wall��
��
� �
リスト33 heun.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4
5 double d f d t ( double t , double x ) ;6 double ax ( double t ) ;7
8 void heun ( double (∗ f ) ( ) , double t , double ∗x , double h )9 {
10 double hx ;11
12 hx = h∗ f ( t , ∗x ) ;13 ∗x += h ∗ ( f ( t , ∗x )+ f ( t + h , ∗x + hx ) ) / 2 ;14 }15
16 i n t main ( i n t argc , char ∗∗ a r gv )17 {18 double h , t , x ;19
20 i f ( a r g c < 2) {21 p r i n t f ( "Usage: %s h\n" , a r gv [ 0 ] ) ;22 e x i t ( 0 ) ;23 }24 h = a t o f ( a r gv [ 1 ] ) ;25
26 x = 1 . 0 ;27 f o r ( t = 0 ; t <= 1 . 0 ; t += h ) {28 p r i n t f ( "%.3f\t%+.6e\t%+.6e\n" , t , x , x−ax ( t ) ) ;29 heun ( d f d t , t , &x , h ) ;30 }31
32 re turn 0 ;33 }
リスト34 odedef.c
1 # i n c l u d e <math . h>2
3 double d f d t ( double t , double x )4 {5 re turn −x ;6 }7
8 double ax ( double t )9 {
10 re turn exp (− t ) ;11 }
Euler法の例題では,xi と xi+1 の差分を返却する関数を
定義して,mainの繰り返し内部で xi に加算する構成とし
ましたが,今回は xi を xi+1 に書き換える構成としました.
このために xのポインタを渡しています.ポインタ変数 x
を受ける関数内ではその値を参照するためには間接参照演
算子’*’の前置が必要となりやや煩雑となります.しかし,
モジュール性が高くなり,main関数での記述を少なくする
ことが可能です.どちらが良いかは好みの問題かもしれま
せんが,ポインタ変数を理解する練習と考えてください.
目次へ28
Cプログラミング 7 常微分方程式の解法
h = 0.1とした時の実行画面例を示します.
$ ./heun 0.1
0.000 +1.000000e+00 +0.000000e+00
0.100 +9.050000e-01 +1.625820e-04
...
0.800 +4.499753e-01 +6.462928e-04
0.900 +4.072276e-01 +6.579478e-04
1.000 +3.685410e-01 +6.615437e-04
7.3 4次のルンゲ・クッタ法
4次のルンゲ・クッタ法は局所的打ち切り誤差が h5であ
り,正確な方法として一般的に用いられます.
k1 = h f (ti, xi)
k2 = h f
(
ti +h
2, xi +
k1
2
)
k3 = h f
(
ti +h
2, xi +
k2
2
)
k4 = h f (ti + h, xi + k3)
xi+1 = xi +k1 + 2k2 + 2k3 + k4
6
(46)
さて実際にいろいろな問題を解こうとすると,式 (46)を
関数 f (x, t)に応じていちいち書き換えていたのではたまり
ません.そこで Heun法で説明したのと同様に関数へのポ
インタ (*f)()を用いて汎用的な解法関数を作成しましょ
う.概要は以下の通りです.
void rk4 ( double (*f)(),
double t, double *x, double h )
{double k1, k2, k3, k4;
k1 = h*f(t, *x);
k2 = h*f(t + h/2, *x + k1);
k3 = h*f(t + h/2, *x + k2);
k4 = h*f(t + h, *x + k3);
*x += (k1 + 2*(k2 + k3) +k4)/6;
}
この関数 rk4を呼出すときの第 1引数に関数名を渡します.
倍精度実数へのポインタ*xは求める解関数の値 x(t)であ
り, rk4内で書き換えられます.なお,関数の引数は rk4
の中の f(...)に合わせる必要があります.
main関数では,初期値 x = x0 を与えてから,繰り返し
の中で tを h変えながら rk4を呼出します.
double dfdt(double t, double x) {...}
main()
{...
rk4(dfdt, t, &x, h);
t += h;
...
}
例題23 x(t)に関する 1階の常微分方程式の初期値問題
dx
dt= −x, x(0) = 1
を,ルンゲ・クッタ法により解き,解析解 x(t) = e−t と比
較しなさい.
[解] 解答例のソース rk4.cを以下に示します.
リスト35 rk4.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4
5 double dfdt(double t, double x);
6 double ax(double t);
7
8 void _rk4(double (*f)(),
9 double t, double *x, double h)
10 {
11 double k1, k2, k3, k4;
12
13 k1 = h * f(t, *x);
14 k2 = h * f(t + h/2, *x + k1/2);
15 k3 = h * f(t + h/2, *x + k2/2);
16 k4 = h * f(t + h, *x + k3);
17 *x += (k1 + 2*(k2 + k3) + k4)/6;
18 }
19
20 int main(int argc, char **argv)
21 {
22 double x, t, h;
23
24 if (argc < 2) {
25 printf("Usage: %s h\n", argv[0]);
26 exit(0);
27 }
28 h = atof(argv[1]);
29
30 x = 1.0;
31 for (t = 0; t <= 1.0; t += h) {
32 printf("%.3f\t%+.6e\t%+.6e\n", t, x, x-ax(t));
33 _rk4(dfdt, t, &x, h);
34 }
35
36 return 0;
37 }
実行画面例を示します.
h = 0.1とした時の実行画面例を示します.
目次へ29
Cプログラミング 7 常微分方程式の解法
$ ./rk4 0.1
0.000 +1.000000e+00 +0.000000e+00
0.100 +9.048375e-01 +8.196404e-08
...
0.800 +4.493293e-01 +3.256172e-07
0.900 +4.065700e-01 +3.314595e-07
1.000 +3.678798e-01 +3.332411e-07
問題13 オイラー法やホイン法による例題の数値解の誤
差を 4次のルンゲ・クッタ法によるものと比較しなさい.
hを変化させて誤差がどうなっていくかを調べてみなさい.
13の解 刻み幅を 1 × 10−1, 3 × 10−2, · · · , 1 × 10−5とし
た場合の t = 1付近における真値との絶対誤差を 3つの方
法について描いて比較した図を示します.
10-18
10-16
10-14
10-12
10-10
10-8
10-6
10-4
10-2
100
10-7
10-6
10-5
10-4
10-3
10-2
10-1
100
101
abso
lute
err
or
h
eulerheun
rk4
図 12 常微分方程式の数値解の刻み幅と誤差の関係:Euler
法,Heun法,4次の Runge-Kutta法の比較
Euler法は h,Heun法は h2,4次 Runge-Kutta法は h4に
比例して誤差が増えています.この結果は以下のように説
明できます.すなわち,それぞれの方法の(1回の毎の)
局所的な打ち切り誤差 hnとすると,同じ tの値に到達する
まで繰り返し回数が hに半比例して増えることを考慮して
hで割った hn−1 という依存性となるのです.�このような数値計算結果をまとめる上で,シェル(bash, csh),awk,
perl,ruby,python 等のスクリプト言語を 1 つ覚えておくことを薦めます.例えば,shならば,以下のようなスクリプトを作成し,実行させるこで必要なデータファイを得ることができます.
リスト 36 disperror.sh
1 # ! / b i n / sh2
3 exec=$14
5 f o r h i n 0 . 1 0 . 0 3 0 . 0 1 3e−3 1e−3 3e−4 1e−4 3e−5 1e −5; do
6 echo $h ‘ . / $exec $h | t a i l −1 ‘;7 done
� �
./disperror.sh euler > euler.dat��
��
� �
ついでに,データファイルを図にする gnuplot スクリプトの例も示します.
リスト 37 disperr.plt
1 s e t te rm p o s t eps enh c o l o r "Times" s i z e 3 ,62 s e t o u t ’disperr.eps’3 s e t l o g s c a l e xy4 s e t s i z e r a t i o −15 s e t key bot tom6 s e t g r i d7 s e t f o r m a t x "10ˆ{%T}"8 s e t f o r m a t y "10ˆ{%T}"9 s e t y l a b e l "{/=24 absolute error}"
10 s e t x l a b e l "{/Times-Italic=24 h}" o f f s e t 0 , −0.511 s e t p o i n t s i z e 1 . 812
13 p l o t [1 e −7 : 1 0 ] [ 1 e −1 8 : ]\14 "euler.dat" u 1 : ( abs ( $4 ) ) t "euler" p t 9 ,\15 "heun.dat" u 1 : ( abs ( $4 ) ) t "heun" p t 11 ,\16 "rk4.dat" u 1 : ( abs ( $4 ) ) t "rk4" p t 717
18 ! ggv d i s p e r r . eps
7.4 2階常微分方程式
常微分方程式 (37)は,1階です.このままでは,Newton
の運動方程式など物理で最も重要な 2階の常微分方程式が
解けないように見えます.しかし,心配無用,次のように
連立常微分方程式に分解すればよいのです.
v(t) =dx
dt= f (t, x, v), x(0) = x0
dv
dt= g(t, x, v), v(0) = v0
(47)
7.4.1 3次元への拡張
3次元座標系では,~r = (x, y, z)のみを問題にするときに
は 3元ベクトル,~v = (vx, vy.vz)も関連する場合には,6元
ベクトルの値を同時に計算する必要があります.各成分毎
にルンゲ・クッタ法を適用すればいいだけなので,難しく
はありませんが面倒です.そこで,それらの関数を準備し
ました.
目次へ30
Cプログラミング 7 常微分方程式の解法
リスト38 rk4fix.h
1 t y p e d e f s t r u c t {2 double f [ 6 ] ;3 } vec6 ;4
5 void r k 4 f i x v 6 ( vec6 f ( ) , double , double ∗ , double ) ;
リスト39 rk4fix.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e "rk4fix.h"
4
5 void r k 4 f i x v 6 ( vec6 f ( ) ,6 double t , double ∗ r , double h )7 {8 i n t i , n ;9 double t s , r s [ 6 ] , k [ 4 ] [ 6 ] ;
10 double c [ 4 ] = { 1 , 0 . 5 , 0 . 5 , 1 } ;11 vec6 d i f f ;12
13 f o r ( n = 0 ; n < 6 ; n++) {14 r s [ n ] = r [ n ] ;15 }16
17 t s = t ;18 f o r ( i = 0 ; i < 4 ; i ++) {19 i f ( i > 0) {20 f o r ( n = 0 ; n < 6 ; n++) {21 r s [ n ] = r [ n ] + k [ i − 1 ] [ n ] ∗ c [ i ] ;22 }23 t s = t + c [ i ]∗ h ;24 }25 d i f f = f ( t s , r s ) ;26 f o r ( n = 0 ; n < 6 ; n++) {27 k [ i ] [ n ] = h ∗ d i f f . f [ n ] ;28 }29 }30
31
32 f o r ( n = 0 ; n < 6 ; n++) {33 f o r ( i = 0 ; i < 4 ; i ++) {34 r [ n ] += k [ i ] [ n ] / c [ i ] / 6 ;35 }36 }37 }
rk4fixv6()は 6元のベクトルを要素とする 1次元配列
(2階常微分方程式)を返却します.6元ベクトル内の並び
は (~r, ~v)としてます.オブジェクトファイル rk4fix.oを
作成しておきましょう.� �
$ gcc -c rk4fix.c��
��
� �
例題24 (1次元単振動) 2階の常微分方程式として最も
重要なものの一つ,角振動数 ωの 1次元単振動の方程式
d2x
dt2+ ω2x = 0, x(0) = 0, v(0) = ω
∴ x(t) = sin(ωx)
を rk4fixv6()を用いて数値解を求め,解析解と比較しな
さい.
[解] 解答例を示します.
リスト40 rk4v6.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include "rk4fix.h"
5
6 #define Tmax 10
7 #define H 0.01
8 #define W M_PI
9
10 vec6 osc(double t, double r[6])
11 {
12 vec6 ret;
13
14 ret.f[0] = r[3];
15 ret.f[1] = 0;
16 ret.f[2] = 0;
17 ret.f[3] = -W*W*r[0];
18 ret.f[4] = 0;
19 ret.f[5] = 0;
20
21 return ret;
22 }
23
24 int main(int argc, char **argv)
25 {
26 double r[6] = {0,0,0,W,0,0}, t = 0;
27
28 while (t < Tmax) {
29 rk4fixv6(osc, t, r, H);
30 t += H;
31 printf("%f %+f %+f\n", t, r[0], sin(W*t));
32 }
33
34 return 0;
35 }
osc(…) の中に,6 元ベクトル r[6] の各成分 r[0]:x,
r[1]:y, r[2]:z, r[3]:vx, r[4]:vy, r[5]:vz について
微分関係を記述しています.y, z方向に関する量は 0とす
れば,x方向のみの 1次元系を記述できます.
x = vx −→ ret.r[0] = r[3]
y = 0 −→ ret.r[1] = 0
z = 0 −→ ret.r[2] = 0
vx = −ω2x −→ ret.r[3] = −W ∗ W ∗ r[0]
vy = 0 −→ ret.r[4] = 0
vz = 0 −→ ret.r[5] = 0
コンパイルは以下のように実行します.� �
$ gcc -o rk4v6 rk4v6.c rk4fix.o -lm -Wall��
��
� �
例題25 (E× Bドリフト) 直交する電場 E = (0, E, 0)と
磁場 B = (0, 0, B)の中では,質量 m,電荷 qの粒子は磁場
の向き z軸の回りに回転しながら,E × B方向(この例で
は x方向)に一定速度で移動していきます.これを E × B
(イービー)ドリフトと呼びます.以下に解析解を示します.
x(t) =E
Bt − A
ωsin(ωt + θ) +
A
ωsin(θ) + x(0)
y(t) = −A
ωcos(ωt + θ) +
A
ωcos(θ) + y(0)
z(t) = vz(0)t + z(0)
vx(t) =E
B− A cos(ωt + θ)
vy(t) = A sin(ωt + θ)
vz(t) = vz(0)
目次へ31
Cプログラミング 7 常微分方程式の解法
ここに,ω = qB/m,A, θ:任意定数です.4次のルンゲ・
クッタ法を用いた数値解析を実行し,解析解と比較しなさ
い.ただし,定数 m, B, E, qを 1,A = 3,θを 0とし,初
期条件は,x(0) = y(0) = z(0) = vz(0) = 0としなさい.な
お,電磁場中で荷電粒子が受ける力を以下に示します.
F = q(E + v × B)
[解] リスト41 rk4v6EB.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include "rk4fix.h"
5
6 #define M 1
7 #define Q 1
8 #define E 1
9 #define B 1
10 #define A 3
11 #define H 0.1
12 #define Tmax 30
13
14 vec6 EbyB(double t, double r[6])
15 {
16 vec6 ret;
17
18 ret.f[0] = r[3];
19 ret.f[1] = r[4];
20 ret.f[2] = r[5];
21 ret.f[3] = Q*B*r[4]/M;
22 ret.f[4] = Q*(E - B*r[3])/M;
23 ret.f[5] = 0;
24
25 return ret;
26 }
27
28 int main(int argc, char **argv)
29 {
30 double r[6] = {0,0,0,E/B-A,0,0}, W = Q*B/M;
31 double t = 0;
32
33 while (t < Tmax){
34 rk4fixv6(EbyB, t, r, H);
35 t += H;
36 printf("%f %+f %+f\n", t, r[0],
37 E/B*t-A/W*sin(W*t));
38 }
39
40 return 0;
41 }
問題14 単振り子の振れ角 θ(t)
θ = −g
lsin θ
の厳密解を rk4fixv6()を用いて数値的に求めてみなさい.
ただし,g = 9.8 [,m · s−2 ],l = 1 [ m ]とします.
問題15 地表面から小物体を,角度 45度,初速度 v0で投
げあげるたときの xy平面内の方物運動
y = −g,
x0 = y0 = z0 = 0, vx0 = vy0 =v0√
2, vz0 = 0
∴ x(t) =v0√
2t, y(t) = −1
2gt2 +
v0√2
t
-10
-5
0
5
10
-5 0 5 10 15 20 25
y
x
A = 3.0
E
t-A*sin(t), -A*cos(t) + A
図 13 直交する電場と磁場内の荷電粒子の x方向への E×B
ドリフト
の軌跡を rk4fixv6()を用いて求め,解析解と比較しなさ
い.ただし,g = 9.8 [ m · s−2 ],v0 = 10 [ m · s−1 ]とします.
7.4.2 グローバル変数
速度の 2乗に比例する抵抗力(比例定数を mb2 とする)
が働く場合の質量 mの小物体の落下運動の運動方程式は,
y軸を鉛直下向きとして,
y = v = g − b2v2
この微分方程式は解析的に解けて,初期速度 v0 = 0,初期
変位 y(0) = 0とすると,
v(t) =
√
g
b2
tanh βt[
β =√
gb2
]
y(t) =1
b2
log(
cosh βt)
と表されます.
例題26 上記問題について,ルンゲ・クッタ法を用いた数
値解と解析解とを比べなさい.ただし,時間 tは 0 ≤ t ≤ 10
とし,抵抗力の比例定数 b2とステップ幅 hを実行時の引
数として指定できるように考えなさい.
[解] 重力加速度は定数として
#define G 9.8
とマクロ定義するのがよいでしょうが,抵抗力の比例定数 b2
はパラメータとして実行時にオプション指定したいと思うの
が普通です.ルンゲ・クッタ法のサブルーチン(rk4fixv6)
の中で微分関係を記述する関数のポインタ vec6 (*f)()
が指す関数 mov()にはパラメータ b2 を渡すことができま
せん.そこで,これらの関数には外部で宣言された変数を
参照するようにします.今まで,関数に外部から値を持ち
目次へ32
Cプログラミング 7 常微分方程式の解法
込むのは,マクロ定義した定数 か,引数リストの変数の
みに制限していました.しかし,実際には,ソース全体に
共通の変数を宣言することができます.これはグローバル
変数(global variable)と呼ばれます(大域変数,広域変
数,大局変数とも呼ばれます).グローバル変数はソース
ファイルに記述されたどの関数内部でも書き換えが可能で
あり,場合によってはバグの原因となりますので,使用は
避けていました.しかし,多くの関数に共通でしかも値を
動的に変更したい場合には,グローバル変数を使わざるを
得ません.
リスト42 rk4v6B2.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include "rk4fix.h"
5
6 #define Tmax 10
7 #define G 9.8
8 double b2;
9
10 vec6 mov(double t, double r[6])
11 {
12 vec6 ret;
13
14 ret.f[0] = 0;
15 ret.f[1] = r[4];
16 ret.f[2] = 0;
17 ret.f[3] = 0;
18 ret.f[4] = G - b2*r[4]*r[4];
19 ret.f[5] = 0;
20
21 return ret;
22 }
23
24 int main(int argc, char **argv)
25 {
26 double r[6] = {0,0,0,0,0,0};
27 double y, v, t = 0, h = 0.1, gb2, g_b2;
28
29 if (argc < 3) {
30 printf("Usage: %s b2 h\n", argv[0]);
31 exit(0);
32 }
33 b2 = atof(argv[1]);
34 h = atof(argv[2]);
35 gb2=G*b2; g_b2=G/b2;
36
37 while (t < Tmax){
38 rk4fixv6(mov, t, r, h);
39 t += h;
40 y = log(cosh(sqrt(gb2)*t)) /b2;
41 v = tanh(sqrt(gb2)*t) * sqrt(g_b2);
42 printf("%.2e %.4e %.4e %.4e %.4e\n",
43 t, r[4], v, r[1], y);
44 }
45
46 return 0;
47 }
コンパイルと実行画面の例を示します.� �
gcc -o rk4v6B2 rk4v6B2.c rk4fix.o -lm
-Wall��
��
� �
$ ./rk4v6_2 0.01 0.1
1.00e-01 9.7968e-01 9.7968e-01 4.8992e-02 4.8992e-02
2.00e-01 1.9574e+00 1.9574e+00 1.9587e-01 1.9587e-01
3.00e-01 2.9314e+00 2.9314e+00 4.4035e-01 4.4035e-01
...
9.90e+00 3.1178e+01 3.1178e+01 2.4081e+02 2.4081e+02
1.00e+01 3.1186e+01 3.1186e+01 2.4393e+02 2.4393e+02
1.01e+01 3.1193e+01 3.1193e+01 2.4704e+02 2.4704e+02
2列と 3列,4列と 5列が等しいことを確認できます.
目次へ33
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
8 gnuplotを利用した計算結果の可視化
結果を画面上のグラフに表示するためには,本来グラ
フィックライブラリを使うというのが正道かもしれません.
実際に過去に何度も,公的にグラフィックの標準化という
試み(例えば,GKS,CGM)が行われましたが,ついに普
及することはありませんでした.唯一例外的に,SGI(グ
ラフィック業界を席巻した企業)のライブラリをオープン
化した OpenGLのみが普及したといえましょう.
OpenGLを使ったグラフィックは大変楽しいのですが,そ
れだけで半期の講義となってしまいますので,ここではグ
ラフを描くツールを子プロセスで起動してパイプ接続しコ
マンドを送りつける方法を選択します.このような用途に
は Gnuplotがうってつけです.
8.1 データファイルのプロット
8.1.1 データファイルの準備
手始めにファイルに保存されたデータをグラフ化してみ
ましょう.データには,E× Bドリフトの t, x(t), y(t), z(t)を
例に取り上げます.rk4v6EB.cを t, x(t), y(t), z(t)が出力さ
れるように手直ししてください.出力リダイレクトを利用
して,データファイル sample1.datを作成してください.� �
$ rk4v6 1 > sample1.dat��
��
� �
8.1.2 gnuplotの起動:対話モード
GNOME端末などの端末上で,オプションを指定せずに
gnuplotを起動してください.� �
$ gnuplot��
��
� �
すると,図 14のように greeting messageを表示した後に,
gnuplotのプロンプトが表示され,コマンド入力待ちとな
ります.
gnuplotのプロンプト
x11を出力端末に設定
図 14 gnuplotの初期起動画面:対話モードの開始
�今まで,端末からコマンドを起動する場合には次のコマンドが実行
できるようにするため,行末に &を加えて処理をバックグラウンドに回
すようにしてきました.が,gnuplot を対話モードで使用する場合には,
gnuplot が端末を占有しますから,& を行末につけるのは無意味となり
ます.
8.1.3 2次元プロット:plot
2次元のグラフのプロット命令は,plotです.データを
プロットする場合には,ファイル名を引数に指定します.
データファイルには,データが 1行ごとに収められ,各行
は空白文字(タブも可)でフィールドが区切られているこ
とを前提に処理されます.defaultでは第 1列を横軸,第 2
列を縦軸にして散布図を描きます.
plot "データファイル名" オプション
数多くのオプションがありますが,良く使うものを表 2に
示します.
表 2 2次元プロット命令 plotの主なオプション
オプション 文字列 引数
種類 with points | lines | linespoints |etc.
記号種 pointtype 整数
記号サイズ pointsize 実数
線種 linetype 整数
線幅 linewidth 実数
データ列 using 列番号:列番号
作成したデータファイル名をプロットしましょう.gnuplot
の対話モード上で,� �
gnuplot > plot "sample1.dat"��
��
� �
と命令すると,図15のようなWindowが X11の画面に表
れます.
図 15 データファイル”sample1.dat”のプロット
目次へ34
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
オプション文字列は長いので略記できます.例えば,大
きさ 1の○記号を指定して,折れ線と記号の両方を描かせ
るには,以下のように記述します.� �
gnuplot > plot "sample1.dat" w lp pt 6 ps
1.0��
��
� �
with を w,linespoints を lp,pointtype を pt,
pointsizeを psと略記しています.
x(t)を横軸,y(t)を縦軸に指定して軌跡を描いてみましょ
う.x(t), y(t) はデータファイルの第 2,3 列に保存されて
いますから,usingオプションを用いて,折れ線で描かせ
ます.� �
gnuplot > plot "sample1.dat" using 2:3 w l��
��
� �
plot "データファイル名" using n:m
すると,次のような妙な図が表れます(図16).
図 16 ”sample1.dat”のプロット:軌跡 (x, y)を描こうとし
たが,軸範囲の設定が不適切なため,縦に伸びてしまった.
これは軸の範囲を全てのデータが収まるように自動で設
定する機能 autoscaleが働くからです.軸の範囲は以下
のように setコマンドを用いて設定します.
set xrange[端の値:端の値] (yrangeも同様)
縦横比がほぼ 4:3であることから,以下のように軸の範囲
を設定すれば,縦横の縮尺がおおよそ等しい図が得られる
はずです.� �
gnuplot > set xrange [0:40]��
��
gnuplot > set yrange [-15:15]��
��
� �
ついでに,x, y軸に名前をつけましょう.� �
gnuplot > set xlabel "X-axis"��
��
gnuplot > set ylabel "Y-axis"��
��
gnuplot > replot��
��
� �
最後の replot は,最近の plot 命令を実行するコマンド
です.
set xlabel "文字列(x軸名)"
set ylabel "文字列(y軸名)"
replot: 最近のプロットを再描画する命令
8.1.4 複数のプロット
1 つのプロットに複数の線を載せたい場合があります.
この例では,データの密度が高いので散布図では記号が重
なってしまいます.そこで間引いて描かせましょう.デー
タの開始と増分はオプション everyを用いて制御します.
plot every {<point incr>}{:<block incr>}{:<start point>}{:<start block>}{:<end point>}{:<end block>}
ここに blockはデータブロックを指します.gnuplotは一
つのファイルに含まれる複数のデータ(区切りは空行)を
認識します.**point**部の記述により,1つのデータブ
ロックにおいて描画データの開始位置,終了位置,位置の
増分を指定できるというわけです.例えば,散布図で 5つ
毎に記号を描画するなら,
plot ”***” using 2:3 every 5 w p
とすればよいことになります.複数の線を描くには区切り
に半角英数のカンマ「,」を用いて列記します.
plot "データファイル名 1" オプション,\
"データファイル名 2" オプション
オプションが長い場合には行の継続の印にバックスラッシュ
を用いて複数行に分割できます.また,同一データファイ
ルを用いる場合には名前を省略可能です.
8.2 スクリプトファイル
命令行数が多くなってくると修正が繁雑になりますので,
スクリプトを geditなどのエディタで編集(修正も)し,そ
のスクリプトファイル名を引数に渡して gnuplotを立ち上
げてみましょう.このような非対話モードはバッチ(batch)
モードとも呼ばれます.バッチモードでは,スクリプトファ
イルの末尾の命令の実行終了とともに全体が終了します.
すると,X11上に開いたプロットWindowも一瞬の内に終
了してしまいます(開いたかどうかも判らない位短いかも
しれません).
X11上のプロットWindowを暫く眺める余裕を確保する
には,2つの方法があります.
目次へ35
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
• プロット命令以降任意の場所に処理の一時停止コマンド pause sを置く.実数 sは秒単位の停止時間です
が,-1を与えると gnuplotを起動した端末でエンター
キーが押されるまで中断することになります.
• 起動時にオプション-persistを指定する.この場合には,プロットWindowは強制終了させなければなり
ません.
例題27 今までのコマンドを sample1.pltという名前のス
クリプトファイルに記述し,gnuplotをそのスクリプトファ
イル名を引数に与えて起動しなさい.
[解] リスト43 sample1.plt
1 set xrange [0:40]
2 set yrange [-15:15]
3 set xlabel "X-axis"
4 set ylabel "Y-axis"
5 plot "sample1.dat" using 2:3 w l, \
6 "" using 2:3 every 5 w p pt 7 ps 2
7 pause -1
図 17 ”sample1.dat”のプロット:軌跡 (x, y)
問題16 rk4の出力をデータファイルに保存して,Runge-
Kutta法の数値計算結果と解析解を比較する図を作成しな
さい.
16の解 解答例を図18に示します.
8.2.1 3次元プロット:splot
鳥瞰図と呼ばれる 3次元のプロット命令は,splotです.
3 次元的な表面を描くのが default ですが,それは後に説
明するとして,ここでは 2次元プロット plotと同様に,3
次元データ (x, y, z)を結んだ線をプロットしてみましょう.
ファイル名を引数に与え,using n:m:lのように 3つの列
番号を指定します.
splot "データファイル名" オプション
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1
0 0.2 0.4 0.6 0.8 1 1.2 1.4 1.6 1.8 2
y 軸
x 軸
Analytic4th-RK
図 18 rk4の数値計算結果と解析解を比較した図
splot特有のオプションには,z軸に関係するものは当然と
して,図が描かれる z領域の基底位置を設定する,
set ticslevel s(実数)
が重要です.sを 0に設定すると,全領域がプロット領域
として使われます.
例題28 sample1.datの (x(t), y(t), z(t))を座標点とした軌跡
を splotで描きなさい.
[解]
リスト44 sample1s.plt
1 set size square
2 set xrange [0:40]
3 set yrange [-20:20]
4 set zrange [0:40]
5 set xlabel "X-axis"
6 set ylabel "Y-axis"
7 set zlabel "Z-axis"
8 set ticslevel 0
9 splot "sample1.dat" using 2:3:4 w l lw 2, \
10 "" using 2:3:4 every 5 w p pt 7 ps 2
11 pause -1
図 19 ”sample1.dat”の 3次元プロット:軌跡 (x, y, z)
目次へ36
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
8.3 パイプ接続:popen()
データファイルやスクリプトファイルを作成せずに,gnu-
plotの標準入力にコマンドやデータを出力して図を描いて
みましょう.すなわち,親プロセスから gnuplotを起動し
て,その標準入力へパイプを接続するというものです.こ
の方法を応用すれば,比較的速く画面を更新することがで
き,その結果,アニメーションが可能になります.
8.3.1 書き込みパイプのオープン
例題29 書き込みパイプを形成した子プロセスとして gnu-
plotを起動し,y = sin xのプロットを画面表示させなさい.
[解] 以下に例を示します.
リスト45 gnuplot01.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 #define SEC 1000000
6 #define WAIT (10*SEC)
7
8 int main(int argc, char **argv)
9 {
10 FILE *GPLT;
11
12 GPLT = popen("gnuplot", "w");
13 fprintf(GPLT, "plot sin(x)\n");
14 fflush(GPLT);
15 usleep(WAIT);
16 pclose(GPLT);
17
18 return 0;
19 }
10行目:まず,普通のファイルのオープンの場合と同じ
様に,ファイル構造体 FILE * GPLT(もちろん名前は任意
であり,GPLTでなくても良い)を宣言します.12行目:次
に popen()によりパイプ接続された子プロセス (この場合
には gnuplot)を起動します.
GPLT = popen("gnuplot", "w");
ここで注意,popen()では,書き込み専用あるいは読み込
み専用モードでしかオープンできません (読み書き可能に
するには,dup()関数により,ファイルディスクリプタの
複製を生成するという少し複雑な操作が必要).13行目:
gnuplotには,fprintf()を用いてコマンド文字列を書き
込みます.ただし,行の終端の印 “\n”を忘れないように
してください.
fprintf(GPLT, ‘‘command\n’’)
続いて,gnuplotに関数 f (x)を描かせます.これは大変簡
単で,
plot f(x)
とします.f(x)には,gnuplotの組込み関数の組合せを表
記することができます.また,ユーザー関数を定義して使
うこともできます.
gnuplot01は簡単なプログラムで,すぐに終了しますか
ら,gnuplotも同時に終了してしまいます.したがって,画
面表示も一瞬にして消えてしまいます.そこで,usleep()
を使って,終了を遅らせます.ところで,折角 usleepを
用いても,実はほとんど画面表示がされません.なぜかと
いうと,一般に入出力はバファリングされることを思い出
してください.GPLTへの書き込みがバッファされてしま
い,gnuplot01の終了時に強制掃き出しされるまで gnuplot
にはコマンドが送られないのです.そこで,
fflush(GPLT)
が必要となります.fflushによって gnuplotへのコマンドを
即時に送出してこそ usleepが活きるのです.
8.3.2 gnuplotの標準入力:’-’
例題30 主プログラムで計算を実行し,結果を gnuplotに
送ってプロットさせてみなさい.
[解] gnuplotの特別なファイル(標準入力)「’-’」を用い
た例を示します.リスト46 gnuplot02.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <unistd.h>
5
6 #define DIV 200
7 #define SEC 1000000
8 #define WAIT 10*SEC
9
10 int main(int argc, char **argv)
11 {
12 int i;
13 double x, y;
14 FILE *GPLT;
15
16 GPLT = popen("gnuplot", "w");
17 fprintf(GPLT, "plot ’-’\n");
18 for (i = 0; i < DIV; i++) {
19 x = i / (double)DIV;
20 y = cos(4 * M_PI * x);
21 fprintf(GPLT, "%f %f\n", x, y);
22 }
23 fprintf(GPLT, "e\n");
24 fflush(GPLT);
25 usleep(WAIT);
26 pclose(GPLT);
27
28 return 0;
29 }
gnuplotに標準入力から 1行ずつデータを読ませるには,
特別なファイル名「’-’」を使います.’e’で始まる行に出
会うとデータ読み込みが終了します.
目次へ37
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
plot ’-’ ⇐ 標準入力から読み込む指定
x1 y1
...
e ⇐ 標準入力からデータ読み込みの終了
Gnuplotの紹介デモの中には,簡易世界地図(大陸海岸
線)のデータ world.datが入っています.gnuplotのデモに
おいては,このデータを 3次元プロット splotを用いて(地)
球面上に描くデモが展開されます.ここでは,地図帳など
でよく見かける 2次元の世界地図を描いてみることにしま
しょう.
例題31 world.datをそのまま x–y平面に描くプログラム
を作成しなさい(正距円筒図法・方眼図法).
[解] 素直に plot命令でデータファイル world.datの内容を
折れ線で描くように,コマンドを送ればよいだけです.
リスト47 map plain.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char **argv)
6 {
7 FILE *GPLT;
8
9 GPLT = popen("gnuplot", "w");
10 fprintf(GPLT, "set grid;\n");
11 fprintf(GPLT, "plot ’world.dat’ w l\n");
12 fprintf(GPLT, "pause 5\n");
13 fflush(GPLT);
14 pclose(GPLT);
15
16 return 0;
17 }
-100
-80
-60
-40
-20
0
20
40
60
80
100
-200 -150 -100 -50 0 50 100 150 200
"world.dat"
図 20 正距円筒法による世界地図
単純な図法では,緯度の高い地域が拡大されてしまいま
す.そこで地図帳には,用途に応じて面積(正績),2点
間距離(正距),角度(正角)などがそれぞれ正しく表示
されるように工夫された地図が掲載されています.
代表的な正積図法のモルワイデ法では,経度 λ,緯度 α
の地点を次のような座標 (x, y)に変換しています.
x = 2√
2λ
πcos θ
y =√
2 sin θ
sin 2θ + 2θ = π sinα
(48)
例題32 world.datを用いて,モルワイデ法の世界地図を
描いてみなさい.外形(楕円形)も描くようにしなさい.
[解] 緯度 αから非線形方程式 (48)により,θを求めるた
めに newton法を組み込んでみました.
リスト48 map mw.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < f l o a t . h>5 # i n c l u d e < s t r i n g . h>6
7 double newton ( double c )8 {9 double x = 0 , dx ;
10 i n t i ;11
12 f o r ( i = 0 ; i < 2 0 ; i ++) {13 dx = −(x + s i n ( x ) − c ) / ( 1 + cos ( x ) ) ;14 x += dx ;15 i f ( f a b s ( dx ) <= DBL EPSILON ) re turn x ;16 }17
18 re turn 0 ;19 }20
21 void mol lwe ide ( double ∗x , double ∗y )22 /* x, y in [rad] */
23 {24 double t h ;25
26 t h = newton (2∗ a s i n ( 2 ∗ ( ∗ y ) /M PI ) ) ;27 ∗x = s q r t ( 8 ) /M PI ∗ (∗ x )∗ cos ( t h ) ;28 ∗y = s q r t ( 2 ) ∗ s i n ( t h ) ;29 }30
31 i n t main ( void )32 {33 double x , y ;34 char buf [ 8 1 ] ;35 FILE ∗GPLT ;36
37 GPLT = popen ( "gnuplot" ,"w" ) ;38
39 f p r i n t f (GPLT ,40 "set size ratio -1; set nokey;"
41 "unset border; unset xtics; unset ytics;"
42 "set xrange[-sqrt(8):sqrt(8)];"
43 "set parametric; set trange[0:2*pi];"
44 "R=sqrt(2);\n" ) ;45
46 f p r i n t f (GPLT , "plot "47 "2*R*cos(t),R*sin(t) lt 5,"
48 " ’-’ w l lt 3 lw 1\n" ) ;49 f f l u s h (GPLT ) ;50 whi le ( f g e t s ( buf , 80 , s t d i n ) != NULL) {51 i f ( s t r l e n ( buf ) < 2 | | buf [ 0 ] == ’#’ ) {52 f p r i n t f (GPLT , "\n" ) ;53 } e l s e {54 s s c a n f ( buf , "%lf %lf" , &x , &y ) ;55 x ∗= M PI / 1 8 0 ;56 y ∗= M PI / 1 8 0 ;57 mol lwe ide (&x , &y ) ;58 f p r i n t f (GPLT , "%8.4f %8.4f\n" , x , y ) ;59 }60 }61 f p r i n t f (GPLT , "e\n" ) ;62 f p r i n t f (GPLT , "pause 10\n" ) ;63 f f l u s h (GPLT ) ;64 p c l o s e (GPLT ) ;65
目次へ38
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
66 re turn 0 ;67 }
46行目は媒介変数を用いて楕円を描いてます.媒介変数
に設定する gnuplotのコマンドを,43行目に記述していま
す.ついでに媒介変数 tの範囲も設定しています.
set parametric ⇐ 媒介変数モードに設定
50行目で標準入力から 1行ずつ読み込んだデータを,54
行目で実数 x, yに変換し,さらに 21行目から定義した関
数 mollweide()に渡してモルワイデ法の座標値に変換し
ます(55行目).
モルワイデ法の座標に変換する元のデータを入力リダイ
レクトで読み込みませるために以下のように起動します.� �
$ ./map mw < world.dat��
��
� �
すると,図 21のような地図が画面に表れます.
図 21 モルワイデ図法による世界地図
問題17 モルワイデ図法による世界地図に緯度や経度を
表すグリッド線が表示されるよう,修正を加えなさい.
8.3.3 gnuplotによるアニメーション
gnuplotで書き換えを素早く行わせることによって,ア
ニメーションを実現しましょう.
例題33 速度の 2乗に比例する抵抗が働く方物運動の軌
跡を gnuplotを用いて,アニメーション表示してみなさい.
ただし,初期速度を 10 [ m · s−1 ],投げ上げ角度を 60度と
し,抵抗係数 b2 とステップ hを起動時に指定できるよう
にしなさい.なお,運動方程式は以下の通り.
vx = −b2|v|vx
vy = −g − b2|v|vx
[解] 4次のルンゲ・クッタ法による数値計算を用いた例を
示します.
リスト49 rk4v6 3.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < u n i s t d . h>5 # i n c l u d e "rk4fix.h"
6
7 # d e f i n e Tmax 108 # d e f i n e G 9 . 89 # d e f i n e TH0 6 0 . 0
10 # d e f i n e V0 1011 # d e f i n e IVAL 1000012
13 double b2 ;14
15 vec6 mov ( double t , double r [ 6 ] )16 {17 double v ;18 vec6 r e t ;19
20 v = hypo t ( r [ 3 ] , r [ 4 ] ) ;21 r e t . f [ 0 ] = r [ 3 ] ;22 r e t . f [ 1 ] = r [ 4 ] ;23 r e t . f [ 2 ] = 0 ;24 r e t . f [ 3 ] = −b2∗v∗ r [ 3 ] ;25 r e t . f [ 4 ] = −G − b2∗v∗ r [ 4 ] ;26 r e t . f [ 5 ] = 0 ;27
28 re turn r e t ;29 }30
31 i n t main ( i n t argc , char ∗∗ a r gv )32 {33 double r [ 6 ] = { 0 , 0 , 0 , 0 , 0 , 0 } ;34 double y , v , t = 0 , h ;35 FILE ∗GPLT ;36
37 i f ( a r g c < 3) {38 p r i n t f ( "Usage: %s b2 h\n" , a r gv [ 0 ] ) ;39 e x i t ( 0 ) ;40 }41 b2 = a t o f ( a r gv [ 1 ] ) ;42 h = a t o f ( a r gv [ 2 ] ) ;43 r [3 ]=V0∗ cos ( TH0 /1 8 0∗M PI ) ;44 r [4 ]=V0∗ s i n ( TH0 /1 8 0∗M PI ) ;45
46 GPLT = popen ( "gnuplot" ,"w" ) ;47 f p r i n t f (GPLT , "set xrange [0:4]\n" ) ;48 f p r i n t f (GPLT , "set yrange [0:3]\n" ) ;49 whi le ( t < Tmax ) {50 r k 4 f i x v 6 ( mov , t , r , h ) ;51 t += h ;52 f p r i n t f (GPLT , "plot ’-’ pt 7 ps 3\n" ) ;53 f p r i n t f (GPLT , "%f %f\ne\n" , r [ 0 ] , r [ 1 ] ) ;54 f f l u s h (GPLT ) ;55 i f ( r [ 1 ] < 0) break ;56 u s l e e p ( IVAL ) ;57 }58 p c l o s e (GPLT ) ;59
60 re turn 0 ;61 }
41,42行で b2, hを起動オプションから取り込んでます.
43,44行は角度を radianに変換しています.以下のように
コンパイルします.� �
$ gcc -o rk4v6 3 rk4v6 3.c rk4fix.o -lm
-Wall��
��
� �
実行時には,小さい hを指定してみましょう.赤い丸が少
し歪んだ放物線を描いて飛行するアニメーションが表示さ
れるはずです.� �
$ ./rk4v6 3 0.2 0.01��
��
� �
問題18 前記の例題を 3次元空間でアニメーション表示
させてみなさい.
目次へ39
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
8.3.4 リングバッファ
gnuplotを用いて,オシロスコープのような機能を実現
してみましょう.連続した(時系列)データを受け取り,
範囲を少しずつずらしながら描画させれば,オシロスコー
プでデータを表示している画面のように見えます.数値シ
ミュレーションが連続データを発生させる速度は,一般に
gnuplotが 1画面を描画する速度よりも速く,途中にデータ
をため込む機構,すなわちバッファリングが必要です.時
系列データをメモリにすべて保存することは大きさに制限
があって不可能な場合が多いです.むしろ逆に,バッファ
に用いるメモリ領域をある大きさに制限する手だてを工夫
しなければなりません.そこで,いわゆるリングバッファ
を用いてみましょう.
例題34 標準入力から連続して x, yデータを受け取り,そ
れをオシロスコープのように少しずつ範囲をずらしながら
gnuplotで表示させるプログラムを作成しなさい.
[解] gnuplotで表示させるデータ点の数と同じ大きさのリ
ングバッファを用いた例を示します.
リスト50 gplt record ring.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 #define BUFFSIZE 384
5
6 int main(int argc, char *argv[])
7 {
8 int i, im, seek=0;
9 double x, y;
10 double data[BUFFSIZE][2]={{0,0}};
11 FILE *GPLT;
12
13 GPLT = popen("gnuplot -geometry 400x300","w");
14 setlinebuf(GPLT);
15 fprintf(GPLT,
16 "set noxtics; set yrange[-5:5];"
17 "set nokey;\n");
18
19 while(1){
20 fscanf(stdin,"%lf %lf", &x, &y);
21 seek %= BUFFSIZE;
22 data[seek][0]=x;
23 data[seek][1]=y;
24 fprintf(GPLT, "plot ’-’ w l lt 1 lw 2\n");
25 for (i = 1; i < BUFFSIZE; i++){
26 im = (i + seek) % BUFFSIZE;
27 fprintf(
28 GPLT, "%f %f\n",data[im][0],data[im][1]);
29 }
30 fprintf(GPLT,"e\n");
31 seek++;
32 }
33 fclose(GPLT);
34
35 return 0;
36 }
seek が新しいデータを読んで保存する位置を表します
(図 22)到着とともに 1ずつ大きくなっていきますが,剰
余演算
seek++
seek = seek % BUFFSIZE
により, 0 から BUFFSIZE-1 の間に制限することができ
ます.
0 1 N
seek
2
バッファ領域
図 22 リングバッファの概念図
例題35 【カオス振動:duffing方程式】 変位 xの 3乗に
比例する復元力を考慮した,duffing方程式は,カオス振動
を生ずる系として知られています.すなわち,x3に比例す
る復元力と速度に比例する抵抗力が働く振動系に外力を加
えると,一定の周期解に収斂しない振動が生ずる場合があ
り.系は以下の常微分方程式で表されます.
x + ax + bx3 = f cos(ωt) (49)
この振動状態を数値シミュレーションして,時系列データ
t, x(t)を発生させ,gplt record ringにパイプで接続して
振動の経時変化を表示させなさい.
[解] 以下に 4次の Runge-Kutta法 rk4fixv6を用いた例
を示します.リスト51 duffing.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <unistd.h>
5 #include "rk4fix.h"
6
7 double A = 0.05, B = 1.0, F = 7.5;
8
9 vec6 duffing(double t, double r[6])
10 {
11 vec6 ret;
12
13 ret.f[0] = r[3];
14 ret.f[3] = -A*r[3] - B*pow(r[0],3) + F*cos(t);
15 return ret;
16 }
17
18 int main(int argc, char **argv)
19 {
20 double r[6] = {0};
21 double t = 0, h;
22
23 if (argc < 3) {
24 printf("%s F h\n", argv[0]);
25 exit(0);
26 }
27 F = atof(argv[1]);
28 h = atof(argv[2]);
29
30 while(1) {
31 rk4fixv6(duffing, t, r ,h);
32 t += h;
33 printf("%f %f\n", t, r[0]);
目次へ40
Cプログラミング 8 GNUPLOTを利用した計算結果の可視化
34 fflush(stdout);
35 }
36
37 return 0;
38 }
このソースでは,パラメータを a = 0.05, b = 1.0, ω = 1
と固定し,強制振動の振幅 F とステップ hを起動時のオ
プションで指定する仕様となっています.コンパイル時に
rk4fix.oをリンクするのを忘れないでください.� �
$ gcc -o duffing duffing.c rk4fix.o -lm
-Wall��
��
� �
コンパイルがうまくいったら,以下のように実行させます.� �
$ ./duffing 7.5 0.1 |./gplt record ring��
��
� �
duffingは無限ループを形成してますので,終了させるに
は� �Ctrl� �+
� �c� �とキー入力して強制終了させます.
目次へ41
Cプログラミング 9 簡易グラフィックライブラリ EGGX
9 簡易グラフィックライブラリ EGGX
gnuplot はデータをグラフ化するのに,たった数行の命
令で済む,実用的で便利なツールです.かなり無理をして
アニメーションなども作成してみました.もっと自由にグ
ラフィックを操作したいならば,もう一段低位のレベルで
調整が必要となります.Unix系では X Window Systemが
グラフィックシステムを提供しています.今流行のデスク
トップ環境GnomeやKDEも全て,X11(X Window System
Ver.11)の上に構築されています.また将来 X11に取って
代わるグラフィックシステムが登場することは考えにくい
です.従って,X11の最低位のライブラリの関数群がグラ
フィックのシステム(成立ち)を理解するのに最も適して
いることは間違いないのですが,それは専門家になる場合
のお話です.我々は,CGの教科書にあるような事柄を実
現している最上位のライブラリがある程度使えるようにな
ることで満足しましょう.
規模の大きなグラフィックライブラリはこの講義 2回分で
はとても扱えませんので,簡素かつ厳選された機能に絞っ
たものをとりあげます.EGGXは,Plamoという Linuxの
配布パッケージで X11周りを担当されていた山内氏が開
発されたもので,コンパクトでたいへん使い易いライブラ
リです.
+参考ウェブサイト:EGGX/ProCALL
• EGGX/ProCALL:作者のページ
• EGGX/ProCALLでグラフィックス
9.1 EGGX/ProCALLのインストール
9.1.1 ユーザーインストール
システムに EGGXが導入されてない場合には,ユーザー
毎に導入する必要があります.EGGXは規模が小さいので,
必要なファイル数が少なく,とても簡単にインストールで
きます.配布文書 INSTALLに記された通りにインストー
ルを進めます.概要は,ソースから tarで展開されたトッ
プディレクトリに移りリンクで Makefileを作成し,make
で出来あがったヘッダファイルとライブラリとコンパイル
コマンド eggを,作業用のディレクトリにコピーするだけ
です.� �
$ mkdir EGGX��
��
$ tar -zxvf eggx-0.93r3.tar.gz��
��
$ cd eggx-0.93r3��
��
$ make -f Makefile.linux��
��
$ cp egg egg*.h libeggx.a ../EGGX��
��
� �
EGGXという作業ディレクトリを CPR2の下に作成していま
す.ついでに,説明文書もコピーしておきましょう.
� �
$ cp eggx procall.ja.pdf ../EGGX��
��
� �
9.1.2 お試し
今後はディレクトリ˜/CPR2/EGGXで作業をします.した
がって,そこにカレントディレクトリを移します.� �
$ cd ˜/CPR2/EGGX��
��
� �
サンプルファイルをコピーして,お試しコンパイル,実行
をしてみてください(図23).� �
$ cp ../eggx-0.93r3/example tutorial/mousetest.c
./��
��
$ ./egg -o mousetst mousetst.c��
��
gcc -O2 -Wall -o mousetest mousetest.c
-I/usr/local/include -L/usr/local/lib
-I/usr/X11R6/include -L/usr/X11R6/lib -leggx
-lX11 -lm
$ ./mousetst��
��
� �
サンプルファイルの内,rupin.cは,大きなサイズの日
本語文字列を表示するデモですが,フォント設定が適切で
ないと表示されないかもしれません.
図 23 EGGXに含まれるサンプルプログラム mousetest
の実行画面:下の画面でマウスをクリックすると,カーソ
ル線と座標値が表示されます.
目次へ42
Cプログラミング 9 簡易グラフィックライブラリ EGGX
9.2 基本的な機能
以下に述べる説明の詳細については,開発者の山内氏が
提供している簡易マニュアル eggx procall.pdfを参照して
ください.
9.2.1 ウィンドウのオープン
それでは早速ウィンドウを開いてみましょう.それはと
ても簡単にできて,
int win;
...
win = gopen( int xsize, int ysize);
この一行だけで,幅 xsize 高さ ysize の窓が,ルートウィ
ンドウに現れます.fopenや popenと同様に,openしたら
closeするという決まりは守りましょう.
gclose( int wn);
例題36 アスペクト比が 4:3のウィンドウを開いてみな
さい.
[解] 次のような簡単なソースで実現できます.15行目は
お約束.14行目の ggetch(win)はオープンしたウィンド
ウの上でキー入力を待ちます.つまり,一時中断(pause)
の役割を持たせています.これがないと,一瞬のうちにプ
ログラムが終了してしまいます.
リスト52 eggx00.c
1 //#include <stdio.h>
2 //#include <stdlib.h>
3 #include "eggx.h"
4
5 #define WD 384
6 #define HT 256
7
8
9 int main(int argc, char **argv)
10 {
11 int win;
12
13 win = gopen(WD, HT);
14 ggetch(win);
15 gclose(win);
16
17 return 0;
18 }
次のようにコンパイルしてください.� �
./egg -o eggx00 eggx00.c��
��
� �
実行すると,384 × 256の黒い(defaultの背景色)画面
が表示されます.何かキーを入力すると,終了して画面が
消えます.
問題19 起動時にウィンドウサイズをオプション指定で
きるように改変しなさい.
9.2.2 基本図形と文字列
画面の背景を黒以外に変更し,楕円・長方形・文字列な
どを描いてみましょう.オープンした画面の左下点と右上
点の描画時の座標値を設定しなければなりません.これも
簡単にできます.
window( int wn, double xs, double ys, double xe, double ye)
(win,0,0,width,height) とするのが一番わかり易いで
す.背景色を変更するには,
gsetbgcolor( int wn, constant char ∗ argsformat, ...)
とします.argsformatには色の名前(例えば “slate blue”)
や RGB記法(例えば “#993366”)などの文字列を与えま
す.この命令の後に,gclr()で初期化しないと変更には
なりません.前景色を変更するには,以下のコマンドを使
います.
newpen( int wn, int cn) cn = 0,1,· · · ,15newcolor( int wn, constant char ∗ argsformat, ...)
楕円を描くには,
circle( int wn, double xcen, double ycen, double xrad, double yrad)
です.座標値の変数の型は doubleですが,double型や
int型の変数を与えても正常に動作します.他の図形につ
いては,マニュアルを参照してください.
例題37 背景色を黒以外に変更して,楕円などの基本図
形を描いてみなさい.
[解] ”old lace”という名前の白色の背景に,楕円と長方
形と文字列を描いた例を示します.
�X Window System では色を名前で使うことができます.その一覧
/usr/lib/X11/rgb.txt を眺めてみましょう.英語の色の名前が判って
面白いです.
図 24 eggx01の実行画面
目次へ43
Cプログラミング 9 簡易グラフィックライブラリ EGGX
リスト53 eggx01.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include "eggx.h"
4
5 #define WD 384
6 #define HT 256
7
8
9 int main(int argc, char **argv)
10 {
11 int win;
12 double x = 3.14159;
13
14 win = gopen(WD, HT);
15 gsetbgcolor(win, "old lace");
16 gclr(win);
17 window(win, 0, 0, WD, HT);
18
19 newpen(win, 12);
20 circle(win, WD/2, HT/2, WD/4, HT/4);
21 newpen(win, 11);
22 fillrect(win, 0, 0, WD/3, HT/3);
23 newpen(win, 13);
24 drawstr(win, WD/4, 7*HT/8, 16, 0,
25 "displacement x = %f", x);
26
27 ggetch();
28 gclose(win);
29
30 return 0;
31 }
問題20 起動時に背景色をオプション指定できるように
改変してみなさい.
9.2.3 描画速度テスト
EGGXのようなグラフィックライブラリを用いれば,外
部コマンド gnuplotを起動するのに比べて,細かいレイア
ウト調整が可能なだけでなく,描画の速いことが期待でき
ます.
例題38 多数の線分(10000本位)を描画するのに要す
る時間を計測プログラムを作成しなさい.
[解] 多項式の評価(p.17)の例題と同様に,gettimeofday
を利用してみましょう.
リスト54 eggx03.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < s y s / t ime . h>5 # i n c l u d e < s y s / t i m e s . h>6 # i n c l u d e < u n i s t d . h>7 # i n c l u d e <eggx . h>8
9 # d e f i n e WD 40010 # d e f i n e HT 40011 # d e f i n e MG 2012 # d e f i n e IMAX 2000013
14 void d i f f t i m e m s e c ( s t r u c t t i m e v a l s , s t r u c t t i m e v a l f )15 {16 p r i n t f ( "%f [ms]\n" ,17 1000∗ ( f . t v s e c − s . t v s e c )18 + 0 . 0 0 1 ∗ ( double ) ( f . t v u s e c − s . t v u s e c ) ) ;19 }20
21 i n t main ( i n t a r g c , char ∗ a r gv [ ] )22 {23 i n t win , imax , i ;24 double x , y ;25 s t r u c t t i m e v a l t s , t f ;26
27 s r a n d 4 8 ( t i m e s (NULL ) ) ;28 imax = IMAX;29 i f ( a r g c > 1) {30 imax = a t o i ( a r gv [ 1 ] ) ;31 }32
33 g s e t i n i t i a l b g c o l o r ( "white" ) ;34 win = gopen (WD+2∗MG, HT+2∗MG) ;35 window ( win , −MG, −MG, WD+MG, HT+MG) ;36 winname ( win , "Drawing %d lines" , imax / 2 ) ;37
38 g e t t i m e o f d a y (& t s , NULL ) ;39 f o r ( i = 0 ; i < imax ; i ++) {40 x = drand48 ( ) ∗WD; y = drand48 ( ) ∗HT;41 moveto ( win , x , y ) ;42 x = drand48 ( ) ∗WD; y = drand48 ( ) ∗HT;43 newpen ( win , i % 16 ) ;44 l i n e t o ( win , x , y ) ;45 }46 g e t t i m e o f d a y (& t f , NULL ) ;47 d i f f t i m e m s e c ( t s , t f ) ;48 g g e t c h ( ) ;49 g c l o s e ( win ) ;50
51 re turn 0 ;52 }
32 行目で画面の背景色の既定値を変更するコマン
ド gsetinitialbgcolor() を使いました.この関数は,
gopen()よりも前に呼ぶ必要があります.
gsetinitialbgcolor( constant char ∗ argsformat, ...);
問題21 長方形や円などの描画速度を測定するプログラム
を作成しなさい.すなわち,大きさや位置が異なる長方形
や円などを多数描画し時間を計測するプログラムを作成し
なさい.
9.2.4 画像の表示:putimg24()
EGGXは画像を表示すること(あるいはウィンドウを画
像ファイルとして保存すること)も可能です.画像は内容
を EGGXの仕様に合わせた配列としてヘッダファイルな
どに保存しておかなければなりません.コマンドは以下の
ようなものです.
putimg24( int wn, double x, double y, int width,
int height, unsigned char ∗ buf)
画像の幅(width)と高さ(height)が判っていないといけ
ません.buf が配列名です.また (x, y)は左下の位置を指
します.
なお ppm 画像形式を配列に変換するツール ppmtoh が
sampleに含まれています.ppm画像への変換ツールはいろ
いろとありますが,歴史あるNetpbmを利用するといいで
しょう.
目次へ44
Cプログラミング 9 簡易グラフィックライブラリ EGGX
例題39 球と床の ppm画像を使って,方物運動の軌跡を
ストロボ撮影したような画面を表示するプログラムを考え
なさい.
[解] 高機能ペイントツールgimpで,球体と床(長方形を
パターンで塗る)画像を作成し,ヘッダファイルに落とし
たもの(cyan5ppm.h, dunesppm.h)を用意しました.
図 25 画像表示関数 putimg24()を用いた,放物体の運動
の軌跡
リスト55 eggx04.c
1 # i n c l u d e < s t d l i b . h>2 # i n c l u d e <math . h>3 # i n c l u d e <eggx . h>4 # i n c l u d e "cyan6ppm.h"
5 # i n c l u d e "dunesppm.h"
6
7 # d e f i n e WD 4008 # d e f i n e HT 4009 # d e f i n e MG 20
10 # d e f i n e TMAX 1011 # d e f i n e G 9 . 812 # d e f i n e SC 5013
14 i n t main ( i n t a r g c , char ∗ a r gv [ ] )15 {16 i n t win ;17 double x , y , vx0 , vy0 ;18 double t = 0 , h = 0 . 0 5 , V0 = 10 , t h = 6 5 ;19
20 i f ( a r g c > 1) t h = a t o f ( a r gv [ 1 ] ) ;21 i f ( a r g c > 2) h = a t o f ( a r gv [ 2 ] ) ;22 t h ∗= ( M PI / 1 8 0 ) ;23 vx0 = V0∗ cos ( t h ) ;24 vy0 = V0∗ s i n ( t h ) ;25
26 g s e t i n i t i a l b g c o l o r ( "white" ) ;27 win = gopen (WD+2∗MG, HT+2∗MG) ;28 window ( win , −MG, −MG, WD+MG, HT+MG) ;29 winname ( win , "Test for putimg24" ) ;30
31 put img24 ( win , −MG, −MG, 440 , 20 , dunes ) ;32 do {33 x = vx0 ∗ t ;34 y = −G∗ t ∗ t /2 + vy0 ∗ t ;35 put img24 ( win , x∗SC , y∗SC , 12 , 12 , cyan6 ) ;
36 t += h ;37 } whi le ( y >= 0 ) ;38
39 g g e t c h ( ) ;40 g c l o s e ( win ) ;41
42 re turn 0 ;43 }
20,21行目:もし引数がある場合には読み込んで,傾
角,ステップ(シミュレーション時間間隔)と解釈して値
を設定しなおします.解析的に求められ放物運動の軌跡の
座標値 (x(t), y(t))(33,34行目)に,小球の画像を表示さ
ています(35行目).
問題22 速度あるいは速度の 2乗に比例する抵抗が働く
場合の小球の放物運動の軌跡をストロボ撮影したような画
面で表示するプログラムを作成しなさい.
問題23 前問で小球の速度ベクトルも一緒に表示するプ
ログラムに変更してみなさい.ベクトルを表す矢印が込み
合うので,間引いて(何回のステップ毎がよいか適当に設
定する)表示するよう工夫するといいでしょう.
9.3 アニメーション:copylayer()
小球の放物運動をアニメーション表示するには,位置変
化する小球を描いた静止画を次々と切替えて表示すればよ
いのですが,画面の表示と書き換えの同期がずれると,特
に書き換えに時間がかかる場合などには途中の絵が表示
されてしてしまうなど,いろいろ問題が起こります.そこ
で,書き換え中は前の絵を表示しておき,書き換えがすん
だら一辺に表示用のメモリ領域にコピーをして,画面表示
を更新するといった手法がとられます(書き換え用と表示
用にバッファを 2つ持つことからダブルバッファと呼ばれ
ます).layer(), copylayer()は EGGXでこの手法を
実現する関数です.
layer( int wn, int lys, int lyw);
...レイヤー lysを表示
...レイヤー lywに次の絵を裏で書き込む
...
copylayer( int wn, int lyw, int lys);
レイヤー lywからレイヤー lysにコピー
例題40 単振り子の運動(厳密解)をアニメーション表示
するプログラムを作成しなさい.
[解] 数値シミュレーション自身はrk4fixv6(),ちらつきを
抑えたアニメーションはEGGXのlayer(), copylayer()
によって実現する例を示します.
リスト56 eggx pendulum.c
目次へ45
Cプログラミング 9 簡易グラフィックライブラリ EGGX
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e "rk4fix.h"
5 # i n c l u d e "eggx.h"
6 # i n c l u d e "cyan6ppm.h"
7
8 # d e f i n e WD 3209 # d e f i n e HT 320
10 # d e f i n e SC 15011 # d e f i n e Tmax 3012 # d e f i n e G 9 . 813 # d e f i n e L 114
15 vec6 mov ( double t , double r [ 6 ] )16 {17 vec6 r e t ;18
19 r e t . f [ 0 ] = r [ 3 ] ;20 r e t . f [ 3 ] = −G /L∗ s i n ( r [ 0 ] ) ;21
22 re turn r e t ;23 }24
25 i n t main ( i n t argc , char ∗∗ a r gv )26 {27 i n t win ;28 double r [ 6 ] = {M PI / 3 , 0 , 0 , 0 , 0 , 0 } , t = 0 , h=0 . 0 2 ;29 double Th = 60 , th , x , y , X0 = WD/ 2 , Y0 = 3∗HT / 4 ;30
31 i f ( a r g c > 1) Th = a t o f ( a r gv [ 1 ] ) ;32 i f ( a r g c > 2) h = a t o f ( a r gv [ 2 ] ) ;33 r [0 ]=Th /1 8 0∗M PI ;34
35 g s e t i n i t i a l b g c o l o r ( "white" ) ;36 win = gopen (WD, HT ) ;37 window ( win , 0 , 0 , WD, HT ) ;38 newpen ( win , 1 3 ) ;39 l a y e r ( win , 0 , 1 ) ;40
41 whi le ( t < Tmax ) {42 r k 4 f i x v 6 ( mov , t , r , h ) ;43 t += h ;44 t h = Th /1 8 0∗M PI∗ cos ( s q r t (G /L )∗ t ) ;45 x = X0 + s i n ( r [ 0 ] ) ∗L∗SC ;46 y = Y0 − cos ( r [ 0 ] ) ∗L∗SC ;47 g c l r ( win ) ;48 put img24 ( win , x−6 , y−6 , 12 , 12 , cyan6 ) ;49 moveto ( win , X0 , Y0 ) ;50 l i n e t o ( win , x , y ) ;51 c o p y l a y e r ( win , 1 , 0 ) ;52 msleep ( 1 0 ) ;53 }54 g g e t c h ( ) ;55 g c l o s e ( win ) ;56
57 re turn 0 ;58 }
44行で書き込み用レイヤーを 1,表示レイヤーを 0と宣言
しています.52行で画面クリア以降レイヤー 1に描画し,
56行目で表示レイヤー 0にコピーします.57行で時間調
整を行い,表示を保持しています.
問題24 上記の eggx pendulum.cを改変して,微小振動
解 θ(t) = Θ cos(ωt)も同じ画面上に描いて数値計算解(厳
密解)と比較できるようにしなさい.
9.3.1 2つのウィンドウ
EGGXではウィンドウを複数開くことができます.例え
ばシミュレーション自身とその統計結果をそれぞれの窓に
表示すると見映えもよいものができそうです.
例題41 2次元酔歩問題を数値シミュレーションしなさ
い.酔歩の軌跡を表示するウィンドウと,到達点の原点か
らの距離のヒストグラムを表示するウィンドウを別々に開
きなさい(もちろん,1つのウィンドウに全てを描くこと
もできます).
[解] gopen()でウィンドウを 2つ生成する方法例を示し
ます.
リスト57 eggx randomwalk.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <math . h>4 # i n c l u d e < t ime . h>5 # i n c l u d e "eggx.h"
6 # i n c l u d e "cyan4ppm.h"
7
8 # d e f i n e L 3209 # d e f i n e IMAX 1000
10 # d e f i n e IH 5011
12 i n t main ( i n t argc , char ∗∗ a r gv )13 {14 i n t win1 , win2 , ix , iy , i , n , nmax = 5000 , r a ;15 i n t h i s t [ IH ]= { 0 } ;16 double r , hmax = 0 ;17
18 i f ( a r g c > 1) nmax = a t o i ( a r gv [ 1 ] ) ;19 s r a n d 4 8 ( t ime (NULL ) ) ;20 g s e t i n i t i a l b g c o l o r ( "white" ) ;21 // gsetinitialparsegeometry("+%d+%d",0,0);
22 win1 = gopen ( L , L ) ;23 // gsetinitialparsegeometry("+%d+%d",L,0);
24 win2 = gopen ( L , L ) ;25 window ( win1 , −L / 4 , −L / 4 , L / 4 , L / 4 ) ;26 window ( win2 , −10 , −10 , L−10 , L−10) ;27 newpen ( win1 , 1 3 ) ;28 newpen ( win2 , 1 1 ) ;29 l a y e r ( win1 , 0 , 1 ) ;30 l a y e r ( win2 , 0 , 1 ) ;31
32 f o r ( n = 0 ; n < nmax ; n++) {33 g c l r ( win1 ) ;34 g c l r ( win2 ) ;35 moveto ( win1 , 0 , 0 ) ;36 i x = 0 ; i y = 0 ;37 f o r ( i = 0 ; i < IMAX; i ++) {38 r a = ( i n t ) ( 4 . 0 ∗ drand48 ( ) ) ;39 i x += ( r a % 2 ) ∗ ( ra −2 ) ;40 i y += ( ( r a +1) % 2 ) ∗ ( ra −1 ) ;41 l i n e t o ( win1 , ix , i y ) ;42 }43 r = hypo t ( ix , i y ) ;44 i f ( r < IH ∗2) h i s t [ ( i n t ) r /2 ]++ ;45 f o r ( i = 0 ; i < IH ; i ++) {46 i f ( hmax < h i s t [ i ] ) hmax = h i s t [ i ] ;47 }48 d r a w s t r ( win1 , −L /4+7 , −L / 4 , 14 , 0 , "n = %5d" , n+1 ) ;49 put img24 ( win1 , ix −4 , iy −4 , 8 , 8 , cyan4 ) ;50 f o r ( i = 0 ; i < IH ; i ++) {51 d r a w r e c t ( win2 , i ∗6 , 0 , 6 , h i s t [ i ] / hmax ∗ ( L−2 0 ) ) ;52 }53 c o p y l a y e r ( win1 , 1 , 0 ) ;54 c o p y l a y e r ( win2 , 1 , 0 ) ;55 msleep ( 1 0 ) ;56 }57 g g e t c h ( ) ;58 g c l o s e a l l ( ) ;59
60 re turn 0 ;61 }
乱数を発生させて,等確率で xあるいは yの値を ±1し
(40–42行),IMAX歩ませて到達した位置に球を描いてい
ます(51行).ヒストグラムは配列を用意して(17行),
画面におさまるように最大値(47–49行)で正規化して柱
を描いています(52–54行).また,2つのウィンドウが
横に並ぶように root画面上のどこに現れるようにするかを
gsetinitialparsegeometry()で設定しています(23行
目,25行目).
目次へ46
Cプログラミング 9 簡易グラフィックライブラリ EGGX
図 26 2次元酔歩問題の数値シミュレーション.2つのウィ
ンドウを開き,左には酔歩の軌跡自身,右には到達点の原
点からの距離のヒストグラムを描いている.
9.3.2 キー入力
EGGXはマウスやキーボードからの入力を監視する関数
があります.この関数を用いてプログラムの動きを制御す
ることもできます.今まで,中断(pause)の実現に利用し
てきた ggetch()は,実は入力キー値を返却します.
int ggetch()
この値を用いて,例えば switch ∼ case文で多重分岐処理することが簡単にできます.
例題42 図27のように,線分の中央を移動して直角に折
り曲げる操作を繰り返すことにより形成される自己相似図
形(Doragon曲線)を,再帰関数を用いて描いくプログラ
ムを考えなさい.ただし,キー入力によりパラメータを変
化させ,図形がどのように変形するか観察できるようにし
なさい.
z1 z2
z3
z3 = F(z1, z2)
z1
z3 F(z1, z3)
F(z2, z3) z2
図 27 ドラゴン曲線の生成手順
[解] 再帰定義する場合に,θの回転が eiθ をかけるという
操作で表現できる複素平面は便利です.すなわち,z1, z2か
ら z3 を求める再帰的計算式を,
z3
(
=z1 + z2
2+
z2 − z1
2eiπ/2
)
=z1 + z2
2+
z2 − z1
2i
と簡素に定義することができます.
99Cでは(C++だけではなく)複素数が C言語でも使え
るようになりました.できる限り複素数で計算を実行する
ように記述した例を示します.
リスト58 eggx Dragon.c
1 # i n c l u d e < s t d i o . h>2 # i n c l u d e < s t d l i b . h>3 # i n c l u d e <complex . h>4 # i n c l u d e "eggx.h"
5
6 # d e f i n e L 4107 # d e f i n e MG 5 . 08
9 t y p e d e f Complex double Cdbl ;10
11 void Dragon ( i n t win , Cdbl z1 , Cdbl z2 , i n t dim )12 {13 Cdbl z3 ;14
15 i f (0 == dim ) {16 moveto ( win , c r e a l ( z1 ) , cimag ( z1 ) ) ;17 l i n e t o ( win , c r e a l ( z2 ) , cimag ( z2 ) ) ;18 re turn ;19 }20 z3 = ( z2 − z1 ) / 2 ∗ I + ( z1 + z2 ) / 2 ;21 Dragon ( win , z1 , z3 , dim −1 ) ;22 Dragon ( win , z2 , z3 , dim −1 ) ;23 }24
25 void p a r a m i n i t ( Cdbl ∗z1 , Cdbl ∗z2 , i n t ∗ o r d e r )26 {27 ∗ z1 = 0 . 0 ;28 ∗ z2 = 1 . 0 ;29 ∗ o r d e r = 1 0 ;30 }31
32 i n t main ( void )33 {34 i n t win , o r d e r , key = 0 ;35 Cdbl z1 , z2 ;36
37 p a r a m i n i t (&z1 , &z2 , &o r d e r ) ;38 g s e t i n i t i a l b g c o l o r ( "white" ) ;39 win = gopen ( L+2∗MG, L+2∗MG) ;40 window ( win , −MG/L−0 .4 , −MG/L−0 .6 , 1 .2+MG/L , 1.0+MG/L ) ;41 winname ( win , "Dragon Curve" ) ;42 newpen ( win , 1 3 ) ;43
44 whi le ( key != ’q’ ) {45 g c l r ( win ) ;46 Dragon ( win , z1 , z2 , o r d e r ) ;47 d r a w s t r ( win , −0.4 , 0 . 9 , 16 , 0 , "order = %d" , o r d e r ) ;48 key = g g e t c h ( ) ;49 swi t c h ( key )50 {51 c ase ’a’ : o r d e r += 1 ; break ;52 c ase ’z’ : o r d e r −= 1 ; break ;53 c ase ’r’ : p a r a m i n i t (&z1 , &z2 , &o r d e r ) ; break ;54 d e f a u l t : break ;55 }56 }57 g c l o s e ( win ) ;58
59 re turn 0 ;60 }
複素数を使う場合には,3行目のように<complex.h>を
インクルードする必要があります.倍精度複素数 zの宣言
および初期化は以下のようにできます.
Complex doube z = 3.0 + 4.0i;
complex double z = 5 + 4i;
この宣言は長く煩わしいので 9行目で typedefしています.
なお宣言時の初期化ではなく,本文中における代入には
小文字の’i’は使えません.代わって大文字の’I’(虚数
単位)を使います(未定義だったら,自分で定義すること
ができます).
目次へ47
Cプログラミング 9 簡易グラフィックライブラリ EGGX
z = 3.0 + 4.0*I;
多くの数学関数が複素数に対応しています.実数関数と区
別するために先頭に文字’c’をつけた関数名が用意されて
います.
csin(z), cexp(z), csinh(z), clog(), csqrt(z), cabs(z)
etc.
複素数 z = x + iyの実数部 x =Re[z]と虚数部 y = Im[z]は,
creal(), cimag()を用いて取得します.
x = creal(z); y = cimag(z);
以下のような代入はできませんので注意してください.
8 creal(z) = x; cimag(z) = y;
図28に実行画面例を示します.� �a� �を 2回入力して,次
数を 12としたときのものです.
図 28 eggx Dragonの実行画面
問題25 図29のように,ドラゴン曲線と異なる折る方の
繰り返しにより形成される自己相似図形を描いてみなさい.
z1 z2
z3
z3 = F(z1, z2)
z1
z3 F(z1, z3) F(z3, z2)
z2
図 29 C曲線の生成手順
問題26 図30のような正三角形を形造る折り曲げの繰
り返しによって形成される相似図形(Koch曲線)を描き
なさい.
z1 z2z3
[z3 , z4, z5] = F(z1, z2)
z5
z4
F(z1, z3) F(z5, z2)
F(z3, z4) F(z4, z5)
60o
図 30 コッホ曲線の生成手順
問題27 以下のような複素画面上の縮小写像の繰り返し
により,自己相似曲線を描くプログラムを考えなさい.
f0(z) = az + bz, f1(z) = c(z − 1) + d(z − 1) + 1
a, b, c, d:複素定数, z:zの共役複素数
ただし,キー入力によりパラメータを変化させ,図形がど
のように変形するか観察できるようにしなさい.
27の解 繰り返しの次数を N とし,2N までの整数 r
に対してそれぞれ写像による点の移動の仕方を対応させま
す.すなわち,整数 rを低位側にビットシフトして,2進
数表示したときの最小桁(LSB)の値(これは 2で割った
商の偶奇と同じ)により写像 f0,f1 を用いることにして,
N 回移動を行い,最後の位置に点を描くというものです.
リスト59 eggx Leaf1.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <complex.h>
5 #include "eggx.h"
6
7 #define WD 500
8 #define HT 300
9 #define MG 5
10 #define SC 400
11
12 typedef _Complex double Cdbl;
13
14 Cdbl a = 0;
15 Cdbl b = 0.4614 + 0.4614i;
16 Cdbl c = 0.622 - 0.196i;
17 Cdbl d = 0;
18 Cdbl origin = 0;
19 int win, order = 13, range;
20 int gr[256], gg[256], gb[256];
21
22
23 Cdbl f(Cdbl z, int rank, int parity)
24 {
25 if (rank != 0) {
26 if (parity % 2 ) {
27 z = a*z + b*conj(z);
28 } else {
29 z = c*(z-1) + d*(conj(z)-1) + 1;
30 }
31 return f(z, rank-1, parity/2);
32 } else {
33 return z;
34 }
35 }
36
37 void drawfractal()
38 {
39 int i;
目次へ48
Cプログラミング 9 簡易グラフィックライブラリ EGGX
40 Cdbl z;
41
42 gclr(win);
43 for (i = 1; i <= range; i++) {
44 newrgbcolor(win, gr[i%256], gg[i%256], gb[i%256]);
45 z = f(origin, order, i);
46 pset(win, SC*creal(z), SC*cimag(z));
47 }
48 drawstr(win, 0, HT-10, 14, 0,
49 "b = %+6f %+6fi", creal(b), cimag(b));
50 drawstr(win, WD/2, HT-10, 14, 0,
51 "c = %+6f %+6fi", creal(c), cimag(c));
52 }
53
54 int main(void)
55 {
56 int i, key;
57 Cdbl dzr = 0.001 + 0.000i, dzi = 0.000 + 0.001i;
58
59 order = 13;
60 range = (int)pow(2,order);
61
62 for(i = 0; i <256; i++){
63 makecolor(DS9_GREEN, 0, 255, i, gr+i, gg+i, gb+i);
64 }
65 gsetinitialbgcolor("white");
66 win = gopen(WD+2*MG, HT+2*MG);
67 window(win, -MG, -MG, WD+MG, HT+MG);
68 winname(win, "Fractal Tree");
69 newpen(win, 13);
70
71 drawfractal();
72 while (1) {
73 key = ggetch();
74 switch(key)
75 {
76 case ’q’: gclose(win); return (0); break;
77 case ’a’: b += dzr; break;
78 case ’z’: b -= dzr; break;
79 case ’s’: b += dzi; break;
80 case ’x’: b -= dzi; break;
81 case ’d’: c += dzr; break;
82 case ’c’: c -= dzr; break;
83 case ’f’: c += dzi; break;
84 case ’v’: c -= dzi; break;
85 case ’r’: c = 0.6200 -0.197i;
86 b = 0.4616 + 0.4616i; break;
87 default: break;
88 }
89 drawfractal();
90 msleep(10);
91 key = 0;
92 }
93 ggetch();
94 gclose(win);
95
96 return 0;
97 }
図 31 複素平面上の縮小写像による自己相似図形:複素パラ
メータは,a = d = 0, b = 0.4614+0.4614i, c = 0.622−0.196i
問題28 Koch曲線を線形縮小写像を用いる方法で描い
てみなさい.また複素パラメータをキー入力で変化させ,
図形が変形する様子が観察できるようにしなさい.Koch曲
線を描く場合は複素パラメータを以下のように設定します.
a = 0, b =1
2+
√3
6i, c = 0, d =
1
2−√
3
6i
図 32 Koch曲線
9.3.3 マウス入力
マウスをクリックした場合のボタン番号と,ウィンドウ上
の位置(スクリーン座標)を取得する関数 ggetxpress()
を用いてみましょう.
int ggetxpress( int ∗ type, int ∗ button, double ∗ x,double ∗ y )
例題43 EGGXのサンプルに含まれる,マンデルブロー
ト集合図形を描くプログラム mandel.cに手を加えて,マ
ウスクリックにより描画領域を拡大・縮小するように改変
しなさい.
[解] マウスドラッグを捕捉するのは難しいので,マウ
スボタン 1(左),3 (右)をクリックした場合,その位
置を中心にしてそれぞれ描画領域を拡大・縮小させ,ボタ
ン 2(中央)をクリックした場合には終了させるように組
んだ例を示します.
リスト60 eggx Mandel.c
1 #include <math.h>
2 #include <eggx.h>
3
4 #define L 400
5 #define MYCOLOR DS9_B
6 #define REALBEGIN -1.5
7 #define IMAGBEGIN -1.0
8 #define INITSIZE 2.0
9 #define LIMIT 1000
10 #define THRES 4.0
11
12 int get_mandel(double real, double imag)
13 {
14 int i;
目次へ49
Cプログラミング 9 簡易グラフィックライブラリ EGGX
15 double xsq, ysq, re = 0, im = 0;
16
17 for (i = 0; i < LIMIT; i++) {
18 xsq = re * re;
19 ysq = im * im;
20 if (THRES < (xsq + ysq))
21 break;
22 im = 2 * re * im + imag;
23 re = xsq - ysq + real;
24
25 }
26 if (i == LIMIT)
27 return 0;
28 else
29 return i;
30 }
31
32 void draw_mandel(
33 int win, double xs, double ys, double size)
34 {
35 int n, i, j, color_r, color_g, color_b;
36 double step, a, b;
37 winname(win, "Mandelbrot real: %5.3g-%5.3g "
38 "imag: %5.3g-%5.3g",
39 xs, xs + size, ys, ys + size);
40 gclr(win);
41 step = size / L;
42
43 b = ys;
44 for (i = 0; i < L; i++) {
45 a = xs;
46 for (j = 0; j < L; j++) {
47 n = get_mandel(a, b);
48 if (0 < n) {
49 makecolor(MYCOLOR, log(1), log(LIMIT), log(n),
50 &color_r, &color_g, &color_b);
51 newrgbcolor(win, color_r, color_g, color_b);
52 pset(win, j, i);
53 }
54 a += step;
55 }
56 b += step;
57 }
58 }
59
60 int main()
61 {
62 int win, button = 0, type;
63 double x, y, xp, yp, xs, xf, ys, yf, size;
64
65 win = gopen(L, L);
66 xs = REALBEGIN;
67 ys = IMAGBEGIN;
68 xf = REALBEGIN + INITSIZE;
69 yf = IMAGBEGIN + INITSIZE;
70 size = INITSIZE;
71 draw_mandel(win, xs, ys, size);
72
73 while (button != 2) {
74 ggetxpress(&type, &button, &x, &y);
75 xp = xs + size * (x - xs) / L;
76 yp = ys + size * (y - ys) / L;
77 switch (button) {
78 case 1:
79 size *= 0.5;
80 break;
81 case 3:
82 size *= 2.0;
83 break;
84 default:
85 break;
86 }
87 xs = xp - 0.5 * size;
88 ys = yp - 0.5 * size;
89 draw_mandel(win, xs, ys, size);
90 }
91 gclose(win);
92 return 0;
93 }
get mandel() が収束判定結果を計算する関数で,
draw mandel がその結果を元に彩色して図形を描く関数
です.72~89 行の while文が,マウスの入力により描画
領域を再計算して,描画関数 draw mandelを呼び出す無
限ループ部分です.ggetxpress() 関数にはポインタを渡
すので,x,yは doubleで宣言しなければなりません.
図 33 eggx Mandelの初期画面
目次へ50
Cプログラミング 10 GUIツールキット:TCL&TK
10 GUIツールキット:Tcl&Tk
コマンドの起動やファイルの選択をどのように実行する
かは歴史的な経緯があります.マウスが考案されてなかっ
た頃は,コマンド名などをキー入力するしか方法がなかっ
たのですが,現在の主流はメニューボタンをマウスでクリッ
クして実行させるというグラフィカルなユーザーインター
フェース(GUI)です.X Window System はもちろんマウ
スをサポートしており GUIを提供できますが,プログラム
するとなるとそれなりに手間が掛かります.
起動ボタンとオプションの数値を入力するエントリーや
スクロールバーというようなユーザーインターフェース
の機能を自作のプログラムにあまり面倒なソースを記述
せずに付加したいとき,Tcl&Tkは最適なツールです.特
に,widgetと呼ばれるグラフィカルな部品を扱う tkは,
Perl, Python, Rubyなど主だったスクリプト言語の GUI
モジュールやエンジンとして利用されています.
+ 参考ウェブサイト:Tcl&Tk
• John K. Ousterhout:Tcl&Tk の生みの親 J.K.Ousterhout博士のページ
• Tcl Developer Xchange:2000年よりオープンソースとなった,Tcl&Tkの開発者のためのサイト
• Perl/Tk FAQ• TkInter:Pythonの事実上の標準 GUIパッケージ• Rubyist Magazine: Ruby/tkの動向
10.1 Tkインタプリタ:wish
どのような部品があるか,Tcl&Tk(実際には wish)の
デモをみるために,次のようにキー入力して,widgetと
いうスクリプトファイルを実行させましょう.� �
$ /usr/share/tk8.4/demos/widget��
��
� �
すると,部品のデモの一覧が現れます.この中から
Entries and Spin-Boxesの「4.Spin-Boxes」をクリッ
クしてみましょう.三角形のボタンを押して数値を増減さ
せる widgetのデモを体験できます(図34).
デザインが少し古いかもしれませんが(しかも残念なが
ら,デザインを変更することは困難です),まあ我慢しま
しょう.
10.1.1 まずは終了ボタン
GUI部品の tkまで含んだ tclのコマンドインタラプタ
wishを使ってみましょう.いつもならば対話モードで練
習して…となりますが,今回はいきなりスクリプトファイ
ルを作成しましょう.判別にためにファイル名には拡張子
.wshをつけることにします.
図 34 Tcl&Tkの GUI部品 spinboxのデモ画面
� �
$ gedit sample01.wsh��
��
� �
内容は以下のように記述します.最初の行はおまじない
であってもなくても構いません.実質はたった 2行です.
3行目で組み込み部品 buttonを 1個宣言しています.一
般に書式の構造は単純で(属性指定は長いのですが)次の
ようになっています.
部品名(ex. button) 名前 属性
Quitというラベルを表示し,押すと終了コマンド exitを
実行する,.bという名前の buttonを宣言しました.
4行目が画面にレイアウトを実現するpackコマンドです.
pack 名前(一般には複数個) 属性
.bという名前の buttonを 1個収めました.
リスト61 sample01.wsh
1 #!/usr/bin/wish -f
2
3 button .b -text Quit -command exit
4 pack .b
実行するには,他のスクリプト言語と同様にインタプリ
タ wish にスクリプトファイル名 "sample01.wsh" を渡
します.� �
$ wish sample01.wsh��
��
� �
�Unix 系の OS では,スクリプトファイル冒頭に
#!コマンド絶対パス名
と記述すると,そのスクリプトファイル全体を解釈するコマンドを自動
的に起動させることが可能です.すなわち,
$ wish sample01.sh��
��
のかわりに,実行属性を付与しておいて
$./sample01.sh��
��
とあたかも独立のコマンドのように実行できるようになります.
目次へ51
Cプログラミング 10 GUIツールキット:TCL&TK
実行例を図35に示します.現れたボタンを押すとたた単
に終了します.
図 35 Tcl&Tkによる 1個の終了ボタンの生成
10.1.2 外部コマンドの起動
外部コマンドは,内部コマンド exec で起動させること
ができます.すなわち,いままで作成したコマンドを起動
するには,
exec 外部コマンド
とします.これを buttonの -commandオプションで指定
すればいいのです.
リスト62 sample02.wsh
1 #!/usr/bin/wish -f
2
3 button .b_quit -text Quit -command exit
4 button .b_run -text Run \
5 -command {exec ./eggx_pendulum &}
6 pack .b_run .b_quit -side left
図 36 左から並べた外部コマンドボタンと終了ボタン
10.1.3 外部コマンドへのオプション値の引渡し
外部コマンドにオプション値を渡すためには,Tcl&Tk
側で変数を用いる必要があります.変数は setコマンドに
より初期化を伴って,以下のように宣言します.代入演算
子を使っていないの最初戸惑いますが,慣れてください.
set 変数名 値
例)set x 11; set y abc; set z "good morning"
変数は文字列として格納されています.従って,空白を含
む文字列は二重引用符で囲む必要があります(単引用符は
不可).標準出力に値を表示するには putsコマンドを用
います.ドルマークを変数名の前に付けて (シェルと同
様の記法です),以下のように記述します.
puts $変数名
例)puts $x; puts $y; puts $z
マウスで値を設定するために spinboxという部品があり
ます.その値を得るにはウィジェットコマンド getを用い
ます.
ウィジェット get
getを,ファイル入力コマンド getsと混用しないように
気をつけましょう.取り出した値を変数に格納するには,
コマンド置換を用います.コマンドを"[command]"のよう
にブラケットで囲むと,実行した結果の文字列に置換され
るというものです.よって,
set x [ウィジェット get]
と記述して,ウィジェットの値を変数 xに格納することが
できます.
ようやく準備ができました.
例題44 eggx pendulumの起動オプションに初期角度
が設定されているものとし,Tcl&Tk側の起動ボタンのコ
マンドにその値を付加して起動できるようにプログラムを
構成しなさい.オプション初期角度の値は spinboxを利
用してマウスで全ての操作が行えるよう考えなさい.
[解]
リスト63 sample03.wsh
1 #!/usr/bin/wish -f
2
3 set angle 30
4 set cmd ./eggx_pendulum
5
6 label .l_angle -text Angle:
7 spinbox .s_angle -from 5 -to 120 -width 4 \
8 -command {set angle [.s_angle get]; update}
9 .s_angle set $angle
10
11 button .b_quit -text Quit -command "exit"
12 button .b_run -text Run \
13 -command {exec $cmd $angle &}
14
15 pack .l_angle .s_angle .b_run .b_quit -side left
3,4行で初期角度を保持する変数 angleと,起動外部コ
マンド文字列 cmdを宣言しています.7行目,spinboxはオ
プションとして,変数の範囲 -from *** -to *** と表示
幅 -width *** を設定するべきでしょう.コマンドは,値
を変数 angleにセットするというのが順当です.updateコ
マンドは,確実に実行させるためのコマンドです.Tcl&Tk
ではコマンドは適当にバッファされていて必ずしも即時実
行されるわけではないのです.updateはそれまで溜ってい
たコマンドをフラッシュします.spinboxが何を扱ってい
るのかラベルを付ける必要があるので,label ウィジェッ
トを宣言しました(6行).
9行目,spinboxの値を初期化するために,ウィジェッ
トコマンド setを使いました(変数に対する setと書式
が違うので間違えないでください).
目次へ52
Cプログラミング 10 GUIツールキット:TCL&TK
ウィジェット set 値
8 set ウィジェット 値
図37に実行画面例を示します.spinboxで値を設定して
から,Runボタンで起動してみください.
図 37 初期振れ角設定用の spinboxを追加
問題29 eggx pendulum のルンゲクッタ法のステップ
hを設定できるように新たに起動オプションを加えなさい.
Tcl&Tk側でも,対応する spinboxを付け加え,2つのオ
プションを指定して起動するように変更しなさい.
目次へ53
Cプログラミング 11 応用例
11 応用例
11.1 二重振子
図のように,質量 m1, m2の 2個の重りが連結された振
り子を二重振子と呼びます.二重振子では,重りを自然な
状態から運動を開始する(例えば重り 2を持って静止させ
た後にはなす)と鉛直線を含む平面内で運動すると考えて
よいでしょう.
φ1
φ2
L1
L2
O
m1
m2
φ1
φ2
O
φ1
φ2
Q
SR
φ2- φ1
図 38 二重振子:概要(左),重りの速度ベクトル(右)
11.1.1 運動方程式
初等力学では運動方程式を導くのは困難です.解析力学
によれば,φ1, φ2 について以下の運動方程式を得ます.
φ1 = −g
D′L1
[
sin φ1 −cos∆φ sin φ2
η
+sin∆φ
gη
{
L1 cos∆φ φ21 + L2φ
22
}
]
(50)
φ2 = −g
D′L2
[
sin φ2 − cos∆φ sin φ1
− sin∆φ
gη
{
L2 cos∆φ φ22 + ηL1φ
21
}
]
(51)
D′ = 1 − cos2 ∆φ
η, η =
m1 + m2
m2
(52)
例題45 二重振り子を 4次の Runge-Kutta法で数値計
算し,EGGXを用いてアニメーション表示するプログラム
を作成しなさい.ステップ hを変化させて,長時間経過後
の挙動が一致するかどうか確かめなさい.
[解] 簡単のため L1 = L2 とします.m2 の重りを持って
静止させた後に離すという,すなわち φ1(0) = Φ0, φ2(0) =
0, φ1(0) = φ2(0) = 0という初期条件で運動を開始させた場
合を示します.以下のようにコンパイルします.� �
./egg -o eggx dpendulumRK4 eggx dpendulumRK4.c
rk4fix.o��
��
� �
リスト64 eggx dpendulumRK4.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <unistd.h>
5 #include "rk4fix.h"
6 #include "eggx.h"
7 #include "cyan4ppm.h"
8 #include "red6ppm.h"
9
10 #define WD 300
11 #define HT 300
12 #define SC 150
13
14 #define G 9.8
15 #define L 0.5
16 #define IVAL 1000
17
18 double Eta = 5;
19
20 vec6 mov(double t, double r[6])
21 {
22 vec6 ret;
23 double C, S, D;
24 double Df = r[0] - r[1];
25
26 C = cos(Df);
27 S = sin(Df);
28 D = Eta - C*C;
29
30 ret.f[0] = r[3];
31 ret.f[1] = r[4];
32 ret.f[3] = -( G * ( Eta*sin(r[0]) - C*sin(r[1]) )
33 + S * (C*r[3]*r[3] + r[4]*r[4]) )/D ;
34 ret.f[4] = -( Eta * G * (sin(r[1]) - C*sin(r[0]))
35 - S * (Eta*r[3]*r[3] + C*r[4]*r[4]) )/D;
36 return ret;
37 }
38
39 int main(int argc, char **argv)
40 {
41 int win, win2;
42 double r[6] = {0,0,0,0,0,0}, t = 0;
43 double Th = 60, Tmax = 100, h=0.01;
44 double x1, y1, x2, y2, X0 = WD/2, Y0 = 3*HT/4;
45
46 printf("Usage: %s Th{:60} h{:0.01} 1+m1/m2{:5}\n", argv[0]);
47 if (argc > 1) Th = atof(argv[1]);
48 if (argc > 2) h = atof(argv[2]);
49 if (argc > 3) Eta = atof(argv[3]);
50 r[0]=Th/180*M_PI;
51
52 gsetinitialbgcolor("white");
53 win = gopen(WD, HT);
54 window(win, 0,0, WD, HT);
55 win2 = gopen(WD, HT);
56 window(win2, 0,0, WD, HT);
57 newpen(win, 13);
58 newpen(win2, 13);
59 winname(win, "Double Pendulum: RK4");
60 winname(win2, "RK4: ph0 = %.1f, h = %.3e", Th, h);
61 layer(win,0,1);
62
63 while (t < Tmax) {
64 rk4fixv6(mov, t, r, h);
65 t += h;
66 x1 = X0 + sin(r[0])*L*SC;
67 y1 = Y0 - cos(r[0])*L*SC;
68 x2 = x1 + sin(r[1])*L*SC;
69 y2 = y1 - cos(r[1])*L*SC;
70 gclr(win);
71 putimg24(win, x1-6, y1-6, 12, 12, red6);
72 putimg24(win, x2-4, y2-4, 8, 8, cyan4);
73 line(win, X0, Y0, PENUP);
74 line(win, x1, y1, PENDOWN);
75 line(win, x2, y2, PENDOWN);
76 copylayer(win,1,0);
77 pset(win2, x2, y2);
78 usleep(IVAL);
79 }
80 ggetch(win);
81 gclose(win);
82
83 return 0;
84 }
目次へ54
Cプログラミング 11 応用例
図 39 二重振り子の重り 2の軌跡:ステップ hの影響.上
から,h = 0.010, 0.005, 0.002, 0.001
問題30 二重振り子は全エネルギー E が保存される系
です.4 次 Runge-Kutta 法でシミュレーションした場合
に,Eは保存されるのか調べてみなさい.ステップ hへの
依存性を図に表してみなさい.
30の解 僅かながら Eは変化(減少)してしまいます.
1e-16
1e-14
1e-12
1e-10
1e-08
1e-06
0.01 0.1 1 10 100ε
t
図 40 ステップを h0,h0
2,
h0
4,
h0
8(h0 = 0.01)とした時の
全エネルギーの相対誤差 ε =∣
∣
∣1 − E/E0
∣
∣
∣の経時変化
11.1.2 微小振動の解析解
振れ角 φが微小である場合,微分方程式は φ2, φ2の項を
無視し,近似 cos φ ∼ 1, sin φ ∼ φより,
φ1 = −g
D′L1
[
φ1 −φ2
η
]
(53)
φ2 = −g
D′L2
[
φ2 − φ1
]
(54)
D′ = 1 − 1
η, η =
m1 + m2
m2
(55)
となります.変数変換 Φ = φ1 + βφ2 により,
Φ = −ω2Φ (56)
となる基準振動を探してみましょう.G = g/D′ とおいて,
Φ = φ1 + βφ2 = −G
[
φ1
L1
− φ2
L1η+βφ2
L2
− βφ1
L2
]
= −G
[(
1
L1
− βL2
)
φ1 +
(
β
L2
− 1
L1η
)
φ2
]
(57)
の両辺を比べて,満たすべき等式は
ω2 = G
(
1
L1
− βL2
)
(58)
β =
(
β
L2
− 1
L1η
) / (
1
L1
− βL2
)
=β − L2/L1/η
L2/L1 − β
=β − λ/ηλ − β
, λ =L2
L1
(59)
目次へ55
Cプログラミング 11 応用例
βの二次方程式 (59)を解いて,
β2 + (1 − λ)β − λη= 0
∴ β± =λ − 1 ±
√
(λ − 1)2 + 4λ/η
2(60)
すなわち,一般解
φ1 + β+φ2 = A+ sin(ω+t + α+)
φ1 + β−φ2 = A− sin(ω−t + α−)
φ1 =β−A+ sin(ω+t + α+) − β+A− sin(ω−t + α−)
β− − β+φ2 =
A+ sin(ω+t + α+) − A− sin(ω−t + α−)
β+ − β−
(61)
を得ます.
例題46 L1 = L2(= L), m1 = m2 の場合について,微小
振動解と Runge-Kutta法による数値解を比較しなさい.
[解] この場合 λ = 1, η = 2ですから,
β± = ±1√
2, ω± =
√
g
L
(
2 ∓√
2)
(62)
m1 を持って静止させた後に離すという初期条件では,
φ1(0) =A+ sinα+ + A− sinα−
2(63)
φ2(0) =A+ sinα+ − A− sinα−√
2= 0 (64)
φ1(0) =ω+A+ cosα+ + ω−A− cosα−
2= 0 (65)
φ2(0) =ω+A+ cosα+ − ω−A− cosα−√
2= 0 (66)
(65),(66)より,α+ = α− =π
2.これを (63)(64)に代入
して,A+ = A− = φ1(0).結局,
φ1(t) =φ0
2
(
cosω+t + cosω−t)
(67)
φ2(t) =φ0√
2
(
cosω+t − cosω−t)
(68)
という解析解を得ます.
リスト65 dpendulum small.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <unistd.h>
5 #include "rk4fix.h"
6
7 #define G 9.8
8 #define L 1
9
10 double Eta = 2.0;
11
12 vec6 mov(double t, double r[6])
13 {
14 vec6 ret;
15 double C, S, D;
16 double Df = r[0] - r[1];
17
18 C = cos(Df);
19 S = sin(Df);
20 D = Eta - C*C;
21
22 ret.f[0] = r[3];
23 ret.f[1] = r[4];
24 ret.f[3] = ( -S * (C*r[3]*r[3] + r[4]*r[4]) +
25 G * (C*sin(r[1]) - Eta*sin(r[0])) )/D ;
26 ret.f[4] = -( -S * (Eta*r[3]*r[3] + C*r[4]*r[4]) +
27 Eta * G * (sin(r[1]) - C*sin(r[0])) )/D;
28 return ret;
29 }
30
31 int main(int argc, char **argv)
32 {
33 double r[6] = {0,0,0,0,0,0}, t = 0;
34 double Th = 1, Tmax = 100, h=0.01;
35 double r0, r1, R0;
36 double wp, wm;
37
38 wp = sqrt(G/L*(2-sqrt(2)));
39 wm = sqrt(G/L*(2+sqrt(2)));
40 if (argc > 1) Th = atof(argv[1]);
41 if (argc > 2) h = atof(argv[2]);
42 if (argc > 3) Eta = atof(argv[3]);
43 R0 = r[0]=Th/180*M_PI;
44
45 while (t < Tmax){
46 rk4fixv6(mov, t, r, h);
47 t += h;
48 r0 = R0/2*(cos(wp*t) + cos(wm*t));
49 r1 = R0/sqrt(2)*(cos(wp*t) - cos(wm*t));
50 printf("%.4e %-.4e %-.4e %-.4e %-.4e\n",
51 t, r[0], r0, r[1], r1);
52 }
53 return 0;
54 }
-2.0e-02-1.5e-02-1.0e-02-5.0e-030.0e+005.0e-031.0e-021.5e-022.0e-02
90 92 94 96 98 100
φ
t
NumericalAnalytical
-8.0e-04-6.0e-04-4.0e-04-2.0e-040.0e+002.0e-044.0e-046.0e-048.0e-04
0 10 20 30 40 50 60 70 80 90 100
∆φ
t
図 41 二重振り子の微小振動解:解析解と数値解の比較
(上図).解析解と数値解の差(下図).
目次へ56
Cプログラミング A 解析力学
A 解析力学
A.1 二重振り子の Lagrange運動方程式
糸の長さと鉛直線からの振れ角をそれぞれ L1, L2および
φ1, φ2とすると,速度ベクトル間の関係は図38 の右図に
示す三角形 SQRで表されます.従って m2 の速度 v2 は余
弦定理を用いて,
v22 = (L1φ1)2 + (L2φ2)2 + 2(L1φ1)(L2φ2) cos
(
φ2 − φ1
)
(69)
また,位置エネルギー U および運動エネルギー T は
U = −[
m1L1 cos φ1 + m2(L1 cos φ1 + L2 cos φ2)]
g (70)
T =1
2(m1 + m2)L2
1φ21 +
1
2m2L2
2φ2
+ m2L1L2φ1φ2 cos(φ1 − φ2) (71)
解析力学によれば,ラグランジュアン L = T − U をとり,
ラグランジェ運動方程式
d
dt
(
∂L
∂φ1
)
=∂L
∂φ1 ,
d
dt
(
∂L
∂φ2
)
=∂L
∂φ2
(72)
が運動を支配する方程式となります.すなわち,具体的に
は φ1 − φ2 = ∆φとおいて,
d
dt
[
(m1 + m2)L21φ1 + m2L1L2φ2 cos∆φ
]
= −(m1 + m2)gL1 sin φ1 − m2L1L2φ1φ2 sin∆φ (73)
d
dt
[
m2L22φ2 + m2L1L2φ1 cos∆φ
]
= −m2gL2 sin φ2 + m2L1L2φ1φ2 sin∆φ (74)
時間微分を実行して,
(m1 + m2)L21φ1 + m2L1L2 cos∆φ φ2
= −(m1 + m2)gL1 sin φ1 + m2L1L2φ22 sin∆φ (75)
m2L1L2 cos∆φφ1 + m2L22 φ2
= −m2gL2 sin φ2 − m2L1L2φ21 sin∆φ (76)
この φ1, φ2 に関する線形連立方程式を形式的に解いて
Newton運動方程式が得られます.
A.2 二重振り子のHamilton方程式
Runge-Kutta 法では累積誤差の影響で長時間経過後の
挙動がステップ hを変化させると異なってしまい,特にカ
オスになった場合についての信憑性が低いと言わざるを得
ません.二重振り子はエネルギーが保存されるという著し
い性質がありますから,それに適した数値積分方法を用い
る方法が望ましいです.実は,1980 年頃からこのような
エネルギーが保存される系の振舞いを解析する方法として
symplectic法が開発されてきました.この積分を適用す
るには,Lagrange方程式から一歩進んで Hamilton方程
式の形にする必要があります.
H = T + U (77)
qi =∂H
∂pi ,pi = −
∂H
∂qi
(78)
ここに,qi, piはそれぞれ一般座標,広義の運動量であり,
一般座標に付随する広義の運動量はラグランジュアンを用
いて以下のように定義されます.
pi =∂L
∂qi
(79)
二重振り子では具体的に,一般座標として φ1, φ2を選び
ましたから,付随する広義運動量 p1, p2 は
p1 =∂L
∂φ1
= (m1 + m2)L21φ1 + m2L1L2φ2 cos∆φ (80)
p2 =∂L
∂φ2
= m2L22φ2 + m2L1L2φ1 cos∆φ (81)
この式を φ1, φ2 について整理すると,
φ1(φ, p) =m2L2
2 p1 − m2L1L2 cos∆φp2
m2L21L2
2
[
(m1 + m2) − m2 cos2 ∆φ]
=L2 p1 − L1 cos∆φp2
m2L21L2
[
η − cos2 ∆φ] (82)
φ2(φ, p) =(m1 + m2)L2
1 p2 − m2L1L2 cos∆φp1
m2L21L2
2
[
(m1 + m2) − m2 cos2 ∆φ]
=ηL1 p2 − L2 cos∆φp1
m2L1L22
[
η − cos2 ∆φ] (83)
Hamiltonianは
H =1
2(m1+m2)L2
1φ21 +
1
2m2L2
2φ2 + m2L1L2φ1φ2 cos∆φ
− (m1+m2)gL1 cos φ1 − m2gL2 cos φ2 (84)
広義の力 fi = pi は
p1(φ, p) = − ∂H∂φ1
= −(m1 + m2)gL1 sin φ1 + Γ (85)
p2(φ, p) = − ∂H∂φ2
= −m2gL2 sin φ2 − Γ (86)
Γ(φ, p) = m2L1L2φ1φ2 sin∆φ (87)
目次へ57
Cプログラミング B RUNGE-KUTTA法の一般形:陰的 RK法
B Runge-Kutta法の一般形:陰的RK法
ベクトル x(t) = (x1, x2, · · · )についての常微分方程式
x = f (t, x) (88)
に対する s段 Runge-Kutta法は,一般的に以下の形で記
述されます.ここで,段数 (stage)とは中間ベクトルの個
数を意味します.
Xi = xn + h
s∑
=1
ai j f(
tn + c jh, X j
)
, 1 ≤ i ≤ s (89)
xn+1 = xn + h
s∑
i=1
bi f(
tn + cih, Xi
)
, ci =
s∑
j=1
ai j (90)
Butcher記法: c1 a11 · · · a1s
......
...
cs as1 · · · ass
b1 · · · bs
(91)
また,以下のように xよりも f の中間値を用いる方が自然
な記述となる場合もあります.
ki = f(
tn + cih, xn + h
s∑
=1
ai j k j
)
, 1 ≤ i ≤ s (92)
xn+1 = xn + h
s∑
i=1
bi ki, ci =
s∑
j=1
ai j (93)
B.1 陽的RK法
i ≤ jに対して ai j = 0であれば,陽的となります.すな
わち,Bucher記法は以下のような構造となります.
0 0
c2 a21 0...
.... . .
. . .
cs as1 · · · ass−1 0
b1 · · · bs−1 bs
(94)
最もよく使われる古典的 4次の(陽的)Runge-Kutta法は
以下のように記されます.
0 0
1
2
1
20
1
20
1
20
1 0 0 1 0
1
6
1
3
1
3
1
6
(95)
Fehlberg 6段 4(5)次
0 0
1
4
1
40
2
8
3
32
9
320
12
13
1932
2197− 7200
2197
7296
21970
1439
216−8
3680
513− 846
41040
1
2− 8
272 − 3544
2565
1859
4104− 11
400
25
2160
1408
2565
2197
4104− 1
50
16
1350
6656
12825
28561
56430− 9
50
2
55
(96)
Shanks 10段 8次
0 0
4
27
4
270
2
9
1
18
1
60
1
3
1
120
1
40
1
2
1
80 0
3
80
2
3− 13
540 − 1
2
7
9
4
270
1
6
389
43200 − 9
720
966
4320− 1
5
9
1600
1 − 231
200
81
20− 291
5
164
5− 61
1040 0
5
6− 127
2880
9
144− 349
144
114
72− 1
322
1
720
11481
8200 − 81
820
1776
205− 844
205
18
205− 252
41− 3
41
36
410
41
8400 0
9
280
272
840
9
280
9
350
9
35
41
840
(97)
B.2 2段 4次陰的RK法
陽的 4次 RK法と同程度の精度が得られる,陰的 2段 4
次 RK法の Bucher記法は以下のようになります.
3 −√
3
6
1
4
3 − 2√
3
12
3 +√
3
6
3 + 2√
3
12
1
4
1
2
1
2
(98)
また, f が時間 tを含まない場合には,
k1 = f (xn + ha11 k1 + ha12 k2)
k2 = f (xn + ha21 k1 + ha22 k2)(99)
xn+1 = xn + hk1 + k2
2(100)
a11 = a22 =1
4, a12 =
1
4−√
3
6, a21 =
1
4+
√3
6(101)
目次へ58
Cプログラミング B RUNGE-KUTTA法の一般形:陰的 RK法
と書くことができます.もちろん陽的 RK法に比べて格段
に手間が掛かるのですが,陽的 RK法に比べて解の安定性
などが優れており,扱う対象によってはじゅうぶん検討に
値すべき方法です.
B.2.1 調和振動子への適用
ばねの単振動は変位を xとして,微分方程式は
x =
x
v
= f (t, x) =
v
−ω20x
で与えられます.この系の時間発展の数値解析に 2段 4次
の陰的 Runge-Kutta法を適用すると,
k11 = v + ha11k12 + ha12k22 (102)
k21 = v + ha21k12 + ha22k22 (103)
k12 = −ω20 (x + ha11k11 + ha12k21) (104)
k22 = −ω20 (x + ha21k11 + ha22k21) (105)
を得ます.これは線形連立方程式ですから面倒ですが解く
ことができます.(104),(105)式に (102),(103)式を
代入して,整理すると,
0 = x + hc1v +
1
ω20
+h2
24
k12 +h2
2a12k22 (106)
0 = x + hc2v +h2
2a21k12 +
1
ω20
+h2
24
k22 (107)
この 2元連立方程式を解いて,まず
k12 =1
D
[
−
1
ω20
+h2
24
(x + hc1v) +h2
2a12(x + hc2v)
]
k22 =1
D
[
h2
2a21(x + hc1v) −
1
ω20
+h2
24
(x + hc2v)
]
(108)
D =
1
ω20
+h2
24
2
− h4
4a12a21 (109)
を得ます.これを (102),(103)式に代入すれば,k11, k21
も求まります.
ステップ幅 hを同じにした陽的 RK法と陰的 RK法につい
て,全エネルギーの相対誤差の経時変化を比較した結果を
図 42に示します.陰的な方がほぼ 6桁誤差が小さいのは
よいのですが,時間経過とともに増加すること及びその傾
きは陽的な場合とあまり変わってません.
陰的 RK 法のソース例を示します.Bucher 記法の定数
ai j, ciを大域変数で定義し,静的に値を埋め込んでますが,
これはもちろん初期化関数を用意して,その中で
a12 = (3-2*sqrt(3))/12;
1e-18
1e-16
1e-14
1e-12
1e-10
1e-08
1e-06
0.01 0.1 1 10 100
ε
t
Explict 4th RKImplicit 4th RK
図 42 調和振動子の全エネルギーの相対誤差の比較
などと演算結果を代入してもいいでしょう.このソースで
は,bcなど多倍長をサポートしているツールで計算した
桁数の多い値を用いています.
リスト66 eggx harmonicIRK.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <unistd.h>
5 #include "rk4fix.h"
6 #include "eggx.h"
7 #include "cyan10ppm.h"
8
9 #define WD 320
10 #define HT 40
11 #define SC 70
12 #define IVAL 1000
13 #define BOX 20
14
15 double a11 = 0.25;
16 double a12 = -0.03867513459481288225; // (3-2*sqrt(3))/12
17 double a21 = 0.53867513459481288225; // (3+2*sqrt(3))/12
18 double a22 = 0.25;
19 double c1 = 0.21132486540518711774; // (3-sqrt(3)/6
20 double c2 = 0.78867513459481288225; // (3+sqrt(3)/6
21 double K = 4.0;
22 double h2, h24, D;
23
24 void irk4(double t, double *r, double h)
25 {
26 double k11, k12, k21, k22;
27
28 k12 = ( -(1/K+h24)*(r[0]+h*c1*r[1])
29 + 12*h24*a12*(r[0]+h*c2*r[1]) )/D;
30 k22 = ( -(1/K+h24)*(r[0]+h*c2*r[1])
31 + 12*h24*a21*(r[0]+h*c1*r[1]) )/D;
32 k11 = r[1] + h*(a11*k12 + a12*k22);
33 k21 = r[1] + h*(a21*k12 + a22*k22);
34 r[0] += h*(k11 + k21)/2;
35 r[1] += h*(k12 + k22)/2;
36 }
37
38 void drawspring
39 ( int, double, double, double,
40 double, double, double);
41
42 double energy(double *r)
43 {
44 return 0.5*(r[1]*r[1] + K*r[0]*r[0]);
45 }
46
47 int main(int argc, char **argv)
48 {
49 int win;
50 double r[6] = {1,0,0,0,0,0}, t = 0;
51 double xi = 1, Tmax = 100, h=0.01;
52 double x, y, X0 = 3*WD/5, Y0 = BOX;
53 double E0, E;
目次へ59
Cプログラミング C 多変数の NEWTON法
54
55 if (argc > 1) xi = atof(argv[1]);
56 if (argc > 2) h = atof(argv[2]);
57 if (argc > 3) K = atof(argv[3]);
58 r[0] = xi;
59 E0 = energy(r);
60 h2 = h*h; h24=h2/24;
61 D = (1/K + h24)*(1/K + h24) + h2*h2/192;
62
63 gsetinitialbgcolor("white");
64 win = gopen(WD, HT);
65 window(win, 0, 0, WD, HT);
66 newpen(win, 8);
67 winname(win, "Harmonic Oscillator: IRK4");
68 layer(win,0,1);
69
70 while (t < Tmax) {
71 irk4(t, r, h);
72 t += h;
73 E = energy(r);
74 printf("%8.3e % -8.3e\n", t, 1-E/E0);
75 #ifdef EGGX
76 x = X0 + r[0]*SC;
77 y = Y0;
78 gclr(win);
79 putimg24(win, x-10, y-10, 20, 20, cyan10);
80 drawspring(win, BOX, x, y, 15, 6, 8);
81 fillrect(win, BOX-10, Y0-10, 10, 20);
82 copylayer(win,1,0);
83 usleep(IVAL);
84 #endif
85 }
86 #ifdef EGGX
87 ggetch(win);
88 #endif
89 gclose(win);
90
91 return 0;
92 }
ばね振動のアニメーションをさせる場合には,以下のよ
うにばねを描く EGGXのサブルーチン drawspring.oをリ
ンクし,EGGXをコンパイル時のオプションで defineして
ください.� �
./egg -o eggx harmonicIRK eggx harmonicIRK.c
drawspring.o -DEGGX��
��
� �
B.2.2 単振り子への適用
ベクトル x = (φ, ω) と,その微分方程式
x =
φ
ω
= f (t, x) =
ω
− g
Lsin φ
に適用して,以下の非線形連立方程式を得ます.
k11 = ωn + ha11k12 + ha12k22 (110)
k21 = ωn + ha21k12 + ha22k22 (111)
k12 = −g
Lsin (φn + ha11k11 + ha12k21)
= − g
Lsin
[
φn + h(a11 + a12)ωn + h2(a211 + a12a21)k12
+ h2(a11a12 + a12a22)k22
]
(112)
k22 = −g
Lsin (φn + ha21k11 + ha22k21)
= − g
Lsin
[
φn + h(a21 + a22)ωn + h2(a21a11 + a22a21)k12
+ h2(a21a12 + a222)k22
]
(113)
この連立方程式は逐次近似法や Newton法で数値計算する
より他に方法はないでしょう.変数は 2つですから,演習
問題としては適当な面倒さ加減ですから是非各自挑戦して
みてください.C節に「多変数の Newton法]」について概
略をまとめました.
B.2.3 二重振り子への適用
ベクトル x = (φ1, φ2, p1, p2) と,その微分方程式 x = f (x)
に対して適用すると,
k1 = f (xn + ha11 k1 + ha12 k2)
k2 = f (xn + ha21 k1 + ha22 k2)(114)
xn+1 = xn + hk1 + k2
2(115)
a11 = a22 =1
4, a12 =
1
4−√
3
6, a21 =
1
4+
√3
6(116)
k1, k2についての非線形連立方程式 (114)を具体的に書く
のも大変です.もちろん解くことはたいへん困難です.一
般には,x0を決めた時の kiを初期値 k(0)iとして,逐次代入
k(n)i= f
(
x0 + h
s∑
j=1
ai j k(n−1)j
)
(117)
により求めるの方法が実装上簡単になのですが,収束に関
して不明な点が多く信頼性に欠けます.残され道は多変数
の Newton法ですが,これはかなり大変です.
C Newton法による多変数非線形方程
式の解
Newton法を拡張して,以下のような多変数の(非線形)
方程式の解を求めることができます.
f (x) = 0 (118)
具体的には,関数ベクトル f の偏微分を係数とした行列
(ヤコビアン)J を用いて,以下のようなアルゴリズムで
求めることが可能です.
xn+1 = xn − J(x)−1 f (x) (119)
J =∂ f
∂x=
∂ f1
∂x1
∂ f1
∂x2
· · ·
∂ f2
∂x1
∂ f2
∂x2
· · ·
· · · · · ·. . .
(120)
目次へ60
Cプログラミング C 多変数の NEWTON法
C.1 2変数の場合
独立変数を x, yとすると,
f (x, y) =
(
f1
f2
)
(121)
とおいて,ヤコビアン Jの各要素は
j11 =∂ f1
∂x, j12 =
∂ f1
∂y, j21 =
∂ f2
∂x, j22 =
∂ f2
∂y(122)
ヤコビアンの逆行列 Y = J−1 が具体的に求まり,
Y = J−1 =1
j11 j22 − j12 j21
j22 − j12
− j21 j11
(123)
したがって,
∆x = (Y f )1 = Y11 f1(xn, yn) + Y12 f2(xn, yn) (124)
∆y = (Y f )2 = Y21 f1(xn, yn) + Y22 f2(xn, yn) (125)
xn+1 = xn − ∆x (126)
yn+1 = yn − ∆y (127)
という漸化式に従って求めればよいことになります.打ち
切り条件は,∆x, ∆xが共に十分小さい,例えばじゅうぶ
ん小さな値 ǫ に対して,
|∆x| < ǫ and |∆y| < ǫ
を満たした場合に打ち切るとするのが一般的です.
例題47 以下の 2元連立非線形方程式を Newton法で解
きなさい.
2x − sin(1 + x + y) = 0
3y − cos(1 + x − y) = 0
-10 -5 0 5 10
x
-10
-5
0
5
10
y
0.45 0.47 0.49 0.51 0.53 0.55
x
0
0.02
0.04
0.06
0.08
0.1
図 43 関数曲線 2x − sin(1 + x + y) = 0(赤)および
3y − cos(1 + x − y) = 0(緑)
[解] Newton法を行う上で必要な式は以下のとおりです.
f1(x, y) = 2x − sin(1 + x + y)
f2(x, y) = 3y − cos(1 + x − y)
とおいて,ヤコビアン Jは
j11 =∂ f1
∂x= 2 − cos(1 + x + y)
j12 =∂ f1
∂y= − cos(1 + x + y)
j21 =∂ f2
∂x= sin(1 + x − y)
j22 =∂ f2
∂y= 3 − sin(1 + x − y)
|J| = j11 j22 − j12 j21
= 6 − 2 sin(1 + x − y) − 3 cos(1 + x + y)
+ 2 sin(1 + x − y) cos(1 + x + y)
したがって,ヤコビアンの逆行列 J−1 = Y の各要素は,
Y11 =j22
|J|=
3 − sin(1 + x − y)
|J|
Y12 =− j12
|J|=
cos(1 + x + y)
|J|
Y21 =− j21
|J|=− sin(1 + x − y)
|J|
Y22 =j11
|J|=
2 − cos(1 + x + y)
|J|
となります.
目次へ61
Cプログラミング C 多変数の NEWTON法
リスト67 newton multi.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4
5 #define N 20
6
7 int mnewton(double *r, double eps)
8 {
9 double v, w, J, y11, y12, y21, y22;
10 double f1, f2, dx, dy;
11
12 v = cos(1 + r[0] + r[1]);
13 w = sin(1 + r[0] - r[1]);
14 f1 = 2*r[0] - sin(1 + r[0] + r[1]);
15 f2 = 3*r[1] - cos(1 + r[0] - r[1]);
16 J = 6 - 2*w - 3*v + 2*w*v;
17 y11 = (3 - w)/J;
18 y12 = v/J;
19 y21 = -w/J;
20 y22 = (2 - v)/J;
21
22 dx = y11*f1 + y12*f2;
23 dy = y21*f1 + y22*f2;
24 r[0] -= dx;
25 r[1] -= dy;
26
27 if ( (fabs(dx) < eps) || (fabs(dy) < eps) )
28 return 1;
29 else
30 return 0;
31 }
32
33 int main(int argc, char **argv)
34 {
35 int i, flag = 0;
36 double r[2]={0,0}, eps = 1e-8;
37
38 if (argc > 1) eps = atof(argv[1]);
39
40 for (i = 0; i < N; i++) {
41 if ( (flag = mnewton(r, eps)) == 1) {
42 printf("x = %f y = %f\n", r[0], r[1]);
43 printf("df1 = %e, df2 = %e\n",
44 2*r[0] - sin(1 + r[0] + r[1]),
45 3*r[1] - cos(1 + r[0] - r[1]));
46 break;
47 }
48 }
49 if (!flag)printf("Can’t solve\n");
50
51 return 0;
52 }
以下に実行例を示します.
$ ./newton multi
x = 0.499682 y = 0.035456
df1 = -5.854692e-18, df2 = 5.070000e-17
答えは x = 0.499682, y = 0.035456となりました.
目次へ62