Previous ToC Next

5. 運動方程式1 (2020/1/24)

Crystal による数値計算入門5回目です。

説明する文体飽きたのでここで対話形式に。

5.1. 背景説明的導入

赤木 :赤木でございます。15年ぶりくらいの登場かしら?

学生C: 赤木さん誰に向かって喋ってるんですか?私何故ここにいるんでしたっ け?

赤木 :いわゆる人柱かしら。

学生C:あの、、、それはどういう?

赤木 :数値解析というか、運動方程式とかの数値積分入門+プログラミング入 門向けのテキスト、20年くらい前に C++ 想定で書いたんだけど、C++ 言語も この20年間で随分変わったし、他にも色々な言語がでてきたし、ということで 新しくしようと思ったわけ。で、書き始めたのね。だけど何か飽きてきたから、、、

学生C:はい?

赤木 :うん、飽きたから、後は君に書いてもらおうかと思って呼んだの。で、 前の本は A君に付き合ってもらったけど、彼はもうなんか偉くなっちゃってこ んなのに付き合うほど暇じゃなさそうだから、今回は君にね。

学生C:え、前のは紙の本で、ほら、印税半分とか書いてなかったでしたっけ? これそんなのじゃなくて単に赤木さんの趣味っていうか、ウェブページに 載せるだけだからお金はいらないですよね?

赤木 :そこはほらゴニョゴニョをゴニョゴニョして少しは、、、

学生C:えー、最近研究費不正使用とかで色々ありますが、そういうの大丈夫ですか?

赤木 :これはちゃんと目的にあった使用だから問題ないわ。

学生C:じゃあ、そういうことで。

赤木 :でね、最初にいったみたいに前の本では C++ だったんだけど、これで は Crystal にしてみようと思って。

学生C:Crystal ってなんでしたっけ?最近よく人工知能とかで聞くのは Python とか、あと Julia とか聞いたことがありますが、、、

赤木 :Ruby は知ってる?

学生C:はい、Python と似てるとか、日本で開発されたとかくらいですが。

赤木 :Ruby は、名前からもわかるように Perl を念頭において、でも割合ちゃ んとしたオブジェクト指向と動的なインタプリタ言語のよいところをあわせて、 本格的に使えることを目指して開発された言語ね。日本ではユーザーそこそこ いるんだけど、海外だと同じようなことがまあできなくはない Python のほう が普及したかな?

学生C:はあ、で、話はなんでしたっけ? Crystal では?

赤木 :そう、Crystal は、 Ruby とすごく近い文法なんだけど、ちゃんとコ ンパイラがあって実行が速いの。ほとんど同じプログラムが Ruby で実行する のに比べて10倍とか場合によっては100倍とか。

学生C:それはすごそうですが、なんかどマイナーで世界で誰も使ってないと かでは、そんなの勉強しても就職に使えないとかないですか?

赤木 :まあ Ruby も一緒に憶えれば日本では困らないと思うわ。

学生C:本当ですか?

赤木 :多分。

学生C:(うーん、、、、大丈夫かなあ、、、という気もするけど)まあお金が はいるならやってみます。

赤木 :じゃあ、まずは、コンパイラとかのインストール 1 からクラスとかの解説 4 までやってみて。で、わかんないとことかあったら教えて。

学生C:じゃあちょっとやってみます。

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. 課題

  1. 修正版のほうのプログラムを実際に実行してみて、同じ答になることを確 認して下さい。

5.4. まとめ

5.5. 参考資料

Math https://crystal-lang.org/api/0.32.1/Math.html

module https://crystal-lang.org/reference/syntax_and_semantics/modules.html
Previous ToC Next