5.2. 一次元調和振動子
学生C:4 までやってみました。
赤木 :どうだった?難しい?
学生C:難しくないといえば嘘になりますというか、、、なんというか、だからなに?みたいな、、、
赤木 :そうよね。なので、この辺からもうちょっと楽しいことをしましょうよ、というわけ。
学生C:楽しいんですか?
赤木 :まあ人によっては。
学生C:うーん、、、
赤木 :まあしばらく付き合って。というわけで、これやってみて:
一次元調和振動子
について、以下の問題に答えよ。
1 初期条件
からの、 までの数値解を、前進オイラー法、
2次ルンゲクッタ公式(色々あるからどれでも)、リープフロッグ法のそれぞ
れで時間積分するプログラムを作成せよ。時間刻み、数値積分法を
コマンドラインオプションとして入力できるようにせよ。
2 それぞれの方法で、 での誤差を、時間刻みの関数として
グラフ化し、時間の何次になっているか、何故そうなるか議論せよ。
なお、ここでは、数値計算パッケージとかソフトウェアを使うのではなく、自
分でプログラムを書くこと。
赤木 :前進オイラー法とかはそれぞれどういうのか知ってる?
学生C:多分、、、まず前進オイラー法ですね。常微分方程式の初期値問題で
すから、方程式が
で、 で初期値 があって、あと時間刻みが、 とし
ます。 での数値解が
もうちょっと一般に、時刻 での数値解を と書くことにす
れば
で、、、大丈夫でしょうか?
赤木 :うーんと、、、多分。2次ルンゲクッタは?
学生C:同じ微分方程式として、
でしたっけ?
赤木 :まあプログラム書いてみて上手くいけばあってるはずよね。前進オイラー法の原理は?
学生C:教科書的には、もとの常微分方程式の解が存在していてそのテイラー
展開も存在しているとすると、
なわけで、 ですから、テイラー展開の まで、つま
り1次の項をとったものが前進オイラー法です。
赤木 :じゃあ、それで計算したものが、この問題の、 まで計算した時に近似解になるのはどうして?
学生C:え、どうしてって、その、数値解だから近似解なのでは?
赤木 :うーん、、、そうじゃなくて、、、じゃあまずはオイラー法だけでいいから実際にプログラム作って計算してみて?
学生C:はあ、、、やってみます。
(一週間後)
学生C:こんな感じです。
#!/usr/bin/env crystal
include Math
x=1.0; v=0.0; k=1.0
h=ARGV[0].to_f*PI
p! h
t=0
while t< PI*2 - h/2
dv = -x*k*h
x+= v*h
v+= dv
t+= h
end
p! [x, v, t]
p! [x-cos(t), v-sin(t)]
実行結果をいくつか:
gravity> ./harmonic-euler.cr 1e-3
h # => 0.0031415926535897933
[x, v, t] # => [1.0099184201712226, 2.0875749689053084e-5, 6.283185307179335]
[x - (cos(t)), v - (sin(t))] # => [0.009918420171222575, 2.0875749940652505e-5]
gravity> ./harmonic-euler.cr 1e-4
h # => 0.0003141592653589793
[x, v, t] # => [1.0009874475970644, 2.069126097899751e-7, 6.283185307177473]
[x - (cos(t)), v - (sin(t))] # => [0.0009874475970643726, 2.0691472301136493e-7]
gravity> ./harmonic-euler.cr 1e-5
h # => 3.1415926535897935e-5
[x, v, t] # => [1.000098700914577, 2.0672973102662693e-9, 6.283185307165095]
[x - (cos(t)), v - (sin(t))] # => [9.870091457697683e-5, 2.0817890742914563e-9]
gravity> ./harmonic-euler.cr 1e-6
h # => 3.141592653589793e-6
[x, v, t] # => [1.0000098696530915, 2.062700127729346e-11, 6.283185307154872]
[x - (cos(t)), v - (sin(t))] # => [9.86965309146548e-6, 4.5341698913228974e-11]
赤木 :そうねえ、、まず、プログラムの解説してみて。
学生C:では最初から。最初の
#!/usr/bin/env crystal
は、シェルスクリプトとか Ruby のスクリプトで、実行するコマンドを指定す
るやり方を使っています。「#!」のあとにコマンドを書きます。コマンドは
パスみてくれないので、 /usr/bin/env のあとにコマンドを書くのが習慣みた
いです。これ別にあってもなくてもいいんですが、実行の時に crystal と
書かなくてもよくなります。
赤木 :うーん、そうねえ、でも、繰り返して実行するなら、 スクリプトじゃ
ないからちゃんとコンパイルしたほうがいいわ。
crystal build harmonic-euler.cr
で harmonic-euler というコンパイルした実行プログラムができるし、
crystal build --release harmonic-euler.cr
とすると最適化した速いのができるの。
学生C:あ、そういえばそうでした。では最初の行は今後なしで。
赤木 :はい。次の行は?
学生C:
include Math
ですね。これは、数学関数とかを、これがないと Math.cos(x) みたいに書か
ないといけないみたいでちょっと面倒なのでいれてます。
赤木 :Math は言語というか文法的にはどういうもの?
学生C:module ですね。class と module がどう違うのかよくわかってないで
すが、module の中で定義したメソッドは、 class の中で include すると
そのクラスのメソッドになるし、 class の中じゃなくて外だと普通の関数に
なるようです。
赤木 :そうね。次は x, v, k の値ね。それはいいとしてその次は?
学生C: ARGV は、コマンドラインで与えた引数が順番に入る配列です。これ
は C言語の argv ですが、0から引数なのは Ruby と同じです。 PI は
モジュール Math で定義されている円周率で、 2まで積分する
ので入力した数値に円周率かけたものを実際の h にしました。
赤木 :了解。あと残りは?
学生C:数値積分の本体は
while t< PI*2
dv = -x*k*h
x+= v*h
v+= dv
t+= h
end
です。運動方程式と前進オイラー法をそのまま書いてます。
x+= v*h
v-= x*k*h
でもよさそうですが、これだと v を更新する時に、xは既に更新した値になっ
ちゃってるのでオイラー法とは違うものになるので、あらかじめ dv を計算し
てます。
時刻も0から加算していって、 2*PIまできたら止めるとしました。あとは
p! [x, v, t]
p! [x-cos(t), v-sin(t)]
で、配列にして p! で、とすることで変数名とかも一緒にだしてみてます。
cos(t), sin(t) は解析解です。周期 で振幅1の単振動なのでこれで
いいはずです。これとの差で誤差をだしています。
最後に実行結果ですが、hを10倍づつ変えて、誤差をだすと大体1桁づつ小さく
なるので、ちゃんとできてるんではないかと思いますが、どうでしょう?
赤木 :えーと、そうね、一見いいように見えるんだけど、、、t の値は大丈
夫?
学生C:大丈夫というと?
赤木 :ちゃんと になってる?
学生C:え、なってるでしょ?値が 6.2863268998329245、、、あれ?
が 3.141592 として 2倍は 6.283184、、、あれれ?4桁めから違いま
すね。ちゃんと計算すると、、、折角だから電卓に crystal 使ってみます。
gravity> crystal eval "p 6.2863268998329245/(Math::PI)/2"
1.00049999999996
赤木 :それ1ひいて逆数とったら?
学生C:えーと。
gravity> crystal eval "p 1.0/(6.2863268998329245/(Math::PI)/2-1)"
2000.0000001600924
ですね。2000ですね。
赤木 :そう。だから、1ステップ余計に計算しちゃってるの。どうしてかわかる?
学生C:本当は2000ステップちょうどで終わらないといけないんですが、2000
ステップでは にならないということですね。浮動小数点の足し算で
誤差があるから、、、
赤木 :そう。どう直すのがいい?
学生C: 比較する値をちょっと小さいめに、例えば h/2 をひいておくとか、、、
赤木 : それでもいいわね。もうひとつは、はじめから、何ステップで積分す
るかのほうをいれて、hのほうをあとで決めるのね。そっちでやってみて。
学生C: times を使ってみました。n.times{色々} で 「色々」を n 回繰り返します。
include Math
x=1.0; v=0.0; k=1.0
n=ARGV[0].to_i
h = 2*PI/n
p! h
t=0
n.times{
dv = -x*k*h
x+= v*h
v+= dv
t+= h
}
p! [x, v, t]
p! [x-cos(t), v-sin(t)]
実行結果をいくつか:
gravity> crystal build harmonic-euler-v3.cr
gravity> ./harmonic-euler-v3 1000
h # => 0.006283185307179587
[x, v, t] # => [1.0199349143076457, 8.432969374211775e-5, 6.283185307179473]
[x - (cos(t)), v - (sin(t))] # => [0.019934914307645712, 8.432969385516134e-5]
gravity> ./harmonic-euler-v3 10000
h # => 0.0006283185307179586
[x, v, t] # => [1.0019758699537742, 8.284675641517022e-7, 6.28318530718043]
[x - (cos(t)), v - (sin(t))] # => [0.0019758699537741897, 8.284667206271328e-7]
gravity> ./harmonic-euler-v3 100000
h # => 6.283185307179586e-5
[x, v, t] # => [1.0001974115707355, 8.269974924947398e-9, 6.283185307175863]
[x - (cos(t)), v - (sin(t))] # => [0.0001974115707354951, 8.273698413812141e-9]
gravity> ./harmonic-euler-v3 1000000
h # => 6.283185307179587e-6
[x, v, t] # => [1.0000197394036099, 8.271237919989742e-11, 6.283185307155417]
[x - (cos(t)), v - (sin(t))] # => [1.9739403609886352e-5, 1.0688173528613706e-10]
赤木 :tがちゃんと になったようね。はい、今日のところはこの辺かしら。
5.3. 課題
-
修正版のほうのプログラムを実際に実行してみて、同じ答になることを確
認して下さい。
5.4. まとめ
-
crystal build でコンパイルした実行ファイル、crystal build --release
で最適化もしたものができる。
-
include Math で数学定数や数学関数が使えるようになる。Math は module
で、他にも色々な module がある。自分で作ることもできる。
-
ARGF でコマンドライン引数を参照できる。
-
浮動小数点数の演算には誤差があるので、 0.001 を1000回加算しても 1 ぴっ
たりにはならないので注意。
-
n.times{} で繰り返しができる。
5.5. 参考資料
Math https://crystal-lang.org/api/0.32.1/Math.html
module https://crystal-lang.org/reference/syntax_and_semantics/modules.html