2023年度計算数理A演習 -概要-

担当:山田恭史(mail: yamadaya@hiroshima-u.ac.jp)
TA: 鎌迫睦, 原岡郁弥, 福井雅也
場所:水曜日:情報メディアセンター本館2F 端末室,金曜日東図書館2F 端末室
講義時間:第1ターム 水曜 16:20–17:50,金曜 16:20–17:50


【重要】

メディアセンターではLinuxのパソコンを用いて実習を行いますが、自宅でもプログラミングの練習をすることでしょう。皆さん各自が持っているパソコンで快適な python プログラミングを行うために、Anaconda というソフトウェアをインストールします。Anacondaをインストールすることで,spyderも標準装備でついてきます.

https://www.python.jp/install/anaconda/ を開くと Windows版、Mac OS版、Linux版のそれぞれについてインストールの仕方が書かれていますので、それに従ってインストールしておいてください。インストール完了後,第1・2回の講義内容にあるSpyderを使用できるようになるはずですので,確認してください.


お知らせ

(2023/04/10)概要ページの更新を行いました.
(2023/04/10)第1・2回講義ページの更新を行いました.
(2023/04/19)第3・4回講義ページの更新を行いました.


連絡事項はMoodleに掲載しますので、確認しておいてください。


第1・2回:Pythonの基礎
第3・4回:グラフの描
第5・6回:常微分方程式の数値解法(オイラー法)
第7・8回:常微分方程式の数値解法(ルンゲ・クッタ法)
第9回:フーリエ級数展開の実装
第10回:一次拡散方程式の数値解法
第11回:一次拡散方程式の数値解法(その2)
第14・15回:期末レポート

計算数理A演習第1・2回

第1・2回(第1週)


はじめに

計算数理A演習では、プログラミング言語としてPythonを用います。 計算数学演習まではコンパイルを必要とする言語であるC言語を用いていましたが、 本講義ではインタプリタ言語と呼ばれるPythonを用います。

Pythonは近年もてはやされているディープラーニングによってかなり有名になりました。 その理由の一つに,言語自体の設計の新しさと手軽さがあります。

プログラミング言語は慣れてしまえば新しい言語を学ぶことは難しくないですが、初級者である皆さんに二つの言語を学んでもらうことはリスクでもあると思います。

しかし,Pythonの手軽さは新しい言語を学ぶコストよりも手軽に出来ると思いますし,C言語の学習で学んだfor文・if文などの考え方は全く変わりません。 標準語が関西弁に変わったような感覚で書き換えていける程度の差しかありません。 ですのでアルゴリズムについてわからないところがあれば, 計算数学演習のHPはこちらにありますので、参考にしてください。


今週の演習

今日は以下の内容で演習を行います。

  • 本ターム内に,この演習で行うこと
  • 演習のホームページの紹介と見方
  • Pythonの導入

本ターム内に,この演習で行うこと

藤井先生が担当される計算数理Aの講義には毎週必ず出席してください。 この演習の内容は計算数理Aの講義内容に強く依存し、講義内容をよく理解している事を前提条件としています。 演習のみ登録されている方も、できれば講義にも出てください。

演習の当面の目標は、コンピュータを使ってある種の常微分方程式・偏微分方程式を解くことです。 ただし、その為には

  • プログラミング言語の習得
  • 計算結果の可視化技術の習得
  • 数値計算法の習得

といったことが必要となります。

本演習では,Pythonという新しい言語を用いることになりましたが、基本的な数値計算の方法は計算数学演習で学んだことがベースになります。演習序盤(次回から2~3回程度)はPythonの使い方と、グラフィックスライブラリの使い方の演習を行います(予定)。

次に授業で紹介される常微分方程式を数値計算を用いて解いてもらいます。 最終的には、熱方程式(拡散方程式とも呼ばれる)や波動方程式といった偏微分方程式を数値計算で解いてもらい、結果を可視化してもらいます。

演習では、数値計算法の習得も目標の一つではありますが、数値計算の必要性や、それを道具として使うという姿勢について、ある程度理解してもらえればと思います。


演習のホームページの紹介と見方

演習のホームページのトップに「お知らせ」という項目があります。 そこには、レポートの出題状況やその他大事なお知らせを載せますので、毎週必ず見てください。



Python入門

Hello, World!!

C言語でも初めに行うHello, World!!の書き出し方法です。 エディタに次を書き込んで,main.pyとして保存してください。

print("Hello, World!!")

そして、

python main.py

とターミナルで実行してください。

※Pythonには現在2系と3系と呼ばれる異なるバージョンが広まっています。現在は標準で最新の3系がインストールされますが,2系のバージョンを使用している場合プログラムが回らないこともあります.もしも上記方法でプログラムが回らない場合には,ターミナル上で

python -V

と入力してバージョン確認を行い,3系のバージョンへ更新してください。

比較のため、C言語で同じものを書くために必要なプログラムを載せます。

#include <stdio.h>
    int main(){ 
    printf("Hello, World!!\n"); 
    return 0;
}

このプログラムに加えて、コンパイルと実行の二つの処理がさらに必要になります。

このように大幅にコードとして書く量を減らすことができ、結果的にミスを少なく出来るのがこの言語の大きな特徴になります。

さて、この簡単なプログラムからPythonとC言語の二つの言語に見られる違いを述べておきたいと思います。

  • ;(セミコロン)がPythonでは書いてはいけない。
  • main関数が存在しない。

Pythonにはmain関数がないのですが、一応それに対応する考えはありますがここでは触れません。


Spyderを使う

 さらに大きな武器を使ってプログラムの学習を進めていきたいと思います。 これまではエディタでプログラムを書き、ターミナルでコンパイルして実行してファイルにデータを保存して、gnuplotでグラフを書きました。  本演習では、これらのツールがひとまとめになっているソフト(統合開発環境)であるSpyderを使います。言語としてはPythonは言語の書き方としてC言語にかなり近いです。

※2019年度の演習ではJupyter Notebookを使いました.こちらはMathematicaと似て,ざっくりいうとエディタとターミナルが入り混じったような形式でした.SpyderとJupyter Notebookは,どちらも一長一短です.本演習では,基本的に受講者のプログラミング経験は,計算数学演習でのC言語のみであると仮定しています.よって,エディタが分かれていてプログラミング感覚の近いSpyderを利用することとします.ただし,開発環境の違いは本演習の本来の目的には影響しませんので,どちらの形式で進めていただいても構いません.

レポートなどの形式は「〇〇.py」の形のPythonプログラム形式(〇〇.ipynbではない)とします。

それでは、Spyderを実行しましょう。まずターミナルを開いて、

spyder &

と実行してください。

※WINDOWSの場合(2021/4/14追記)

検索欄に”anaconda prompt”と入力し,検索結果からアナコンダプロンプトを開きます
(下図参照).

このアナコンダプロンプトがターミナルの役目を果たします.

(2021/4/14追記) もしも”spyder &”コマンドでエラーになる場合は” &”を取り除き,”spyder”とだけ記述し実行してください.

しばらくすると次の様な画面が現れるはずです。(自動更新に関するアラートはOKしておいてください)

左側がエディタ、右側上部がヘルプで、右側下部がiPythonコンソール(以降、コンソールと呼ぶ)です。コンソールは、とりあえずPython用のターミナルのようなものと覚えておいてください。このコンソール自体でPythonのコマンドを実行できますし、エディタで記述したプログラムファイルを読みこむこともできます。 コンソール下部のタブを選択することで、過去にコンソールに打ち込んだ記録(ログ)を参照することもできます。同様に、ヘルプ下部のタブを選択することで、定義されている変数一覧である変数エクスプローラーや、ファイルエクスプローラーを表示することができます。

Spyderのレイアウトは自由に変更可能です(デモ)。

プログラムの実行

それでは早速使ってみましょう。 デフォルトでは、現在いるディレクトリはホームディレクトリ(/fs/home/アカウント名)です。このままここで作業すると、ホームディレクトリ直下がごちゃごちゃすることになりますので、適当にディレクトリを作成(右クリック→新規→フォルダ)し、移動(ダブルクリック)してください。これでコンソール上でも移動したことになります。

試しに先ほど作成したmain.pyを実行してみましょう。 main.pyを作業ディレクトリに持ってきて、ファイルエクスプローラー上でダブルクリックしてください。すると、左側のエディタにmain.pyが表示されます。左上にある緑の三角(下図参照)をクリックすると、main,pyが実行され、コンソール上に「Hello, World!!」が表示されます。 

ファイルを作成するときは、左上の「ファイル」→「新規ファイル」で作成できます。この段階ではファイルは保存されていないので、「ファイル」→「保存」や「形式を指定して保存」などでファイル名をつけて保存してください。

コンソールの使い方

「Hello, World!!」と表示させたいだけなのであれば、いちいちファイルを使わなくても、interactiveにコンソール上で実行しても良いでしょう(直接コンソール上で

print("Hello, World!")

と打って「return(Enter)」キーを打てば事足ります。他にも、ちょっとした作業であれば、コンソールで事足ります。 ただし後々、行った作業をまとめて1つのファイルにする場合には、コンソール上で行なった作業も追加することを忘れないように。

コンソール上で複数行をまとめて実行したい場合は「ctrl + return(Enter)」で改行できます。

※Macでの注意点
コンソールでreturnを押しても改行されるだけです.コンソールでコマンドを実行する場合は,「Shift+ return」キーで実行できます(Mathematicaと同じ)

コンソールとエディタの使い分けについて

Pythonのプログラムは、「コンソールでの直接入力」も、「エディタでの入力・実行」も同じ結果になります。 今後課題やレポートなどでプログラムを作成することが増えますが、どちらを使っても構いません。例えば、全てコンソールを使って作成しても良いですし、課題ごとに〇〇.pyファイルを作っても良いです。 ただしレポートでは、1個の完結した〇〇.pyファイルとして提出してください。したがって、全てコンソールで作成する場合は、必要な箇所を〇〇.pyファイルにコピーさせる必要があります。このとき必ず全ての変数が意図通りに定義できているか確認してください。提出されたファイルを実行したときに、変数の定義部分がなくて正しく実行できない場合、点数をつけることはできません。また、これ以降登場するnumpyやmatplotlibなどのモジュールの定義、importも忘れないようにしてください。

小ネタ:print文で変数の前後の空白を消すには

例えば

a=1
print("a=", a)

とすると、

a= 1

となり、1の前に空白が入ります。場合によってはこのような空白が入って欲しくないこともあるでしょう。そのときには、

a=1
print("a=", a, sep="")

とすれば、空白がなくなり、

a=1

と表示されます。これは文字列と文字列の区切り(separater)を””で括られた文字列で置き換えることを意味しています。

つまり、

sep="\t"

とすればタブで区切ることもできます。 今後、ケースバイケースで使い分けてみてください。


データ型・変数・初期化

それでは、Pythonの内容に進んでいきますが、C言語との大きな違いが変数にも表れます。

  • 変数の型の宣言が存在しない

C言語ではint型やfloat型の変数の宣言について学びました。 Pythonにも同様に型は存在し、データ型と呼ばれていますが、変数の宣言という

float x;

のような形で変数は宣言できません。 その代わりに宣言と初期化を以下のように行います。

x = 0.0

0.0と書けば小数点があるのでコンピュータが勝手にfloat型と認識してくれます。int型にしたければ、

x = 0

というように整数だけで初期化を行えば良いのです。 実際に変数エクスプローラーで見てみると、それぞれfloat、intとTypeが変わっているのが確認できます。

数値計算では、計算した変数の値を出力する必要があります。 C言語ではint型やfloat型に合わせて、%dや%lfなどと書く必要がありましたが、次のプログラムをCellに書き込んでRunを実行してください。

a = 1
x = 2.1
print(a, x)

変数がそのまま出力されたことがわかると思います。


リスト

計算数学演習では、簡単にですが配列について学びました。 Pythonではリストと呼ばれ、以下のように定義します。 ここでも変数の宣言がないため、初期化を伴います。

X = [1, 3, 5, 7]

配列の要素へのアクセス、及び値の書き換えはC言語と同様に行えます。このとき、配列の番号は0から始まります。 値の3を箱から取り出して出力したければ、

print(X[1])

値の5を書き換えて0にしたければ、

X[2] = 0

と出来ます。ただしこのやり方では100個の要素を持つようなリストを作ることは現実的ではありません。メインにはこの後に学ぶarray型というものを利用します。


制御文

インデント(字下げ)の注意点

C言語での制御文(if文、for文など)は、

for (i=0; i<10; i++) {
    ブロック文
}

のように { } で囲っていました。そのため、{ } 内のインデントが少々ぐちゃぐちゃでも、実行結果には影響を与えませんでした。 以下で学ぶPythonの制御文では、{ } の代わりにインデントで制御文で制御されるブロック文を判別します。そのため、インデントが正しく揃ってない場合、エラーや予期せぬ挙動をします。今一度インデントについて確認しましょう。 Pythonでのインデントは、スペース4個分が標準的です。Spyderでは、タブ(Tab)を押すとスペースが4個分の倍数の位置に止まりますので、タブを使うと良いでしょう。

ともかく、習うより慣れろ、ということで以下の例を見てみましょう。


if文

次にif文について学びます。条件式はifと : の間に挟まれ、制御文はif文のあとインデントされている行が全てif文で制御される範囲になります。

早速以下のサンプルコードを実行してみましょう。さらに、a=1に変えて実行し直してみてください。

a = 0
if a == 0: 
    print(1, a) 
    a = a + 2 
    print(2, a)
    a = a + 10
    print(3, a)

if文の制御から外すには,if文の初めのところとインデントを揃える必要があります。

それでは、if文による分岐の大事な要素であるif-else文についてです。 先のサンプルコードに少し変更を加えるとif-else文が完成します。

先と同様に最初の a=0 を a=1 に変えてみてください。

a = 0
if a == 0: 
    print(1, a) 
    a = a + 2
    print(2, a)
else: 
    a = -11 
    print(3, a)
    a = a + 10
    print(4, a)

それでは、条件が複数ある場合についてです。 C言語では if~else if~ else 文でしたが、二つ目以降の条件が elif と省略されます。

a = 0
if a == 0: 
    print(1, a) 
    a = a + 2 
    print(2, a)
elif a == 1: 
    print(3, a)
else:
    a = -11 
    print(4, a)
    a = a + 10
    print(5, a)

比較演算子と複数条件

以下の表はC言語と全く同じです。

演算子説明記述例記述の意味
>大きいif (a>10):aが10より大なら
>=大きいか等しいif (a>=10):aが10以上なら
<小さいif (a<10):aが10より小なら
<=小さいか等しいif (a<=10):aが10以下なら
==等しいif (a==10):aが10なら
!=等しくないif (a!=10):aが10でないなら

しかし,複数の条件を条件式に入れる場合は以下のようになります。

演算子対応するC言語演算子記述例記述の意味
and&&if a==0 and b==1:aが0かつbが1
or||if a==0 or b==1:aが0またはbが1
not!if not a==1:aが0ではない


課題1

2つの実数 s, t を入力すると、その差の絶対値を表示するプログラムを作成せよ (if 文を使うこと).

Pythonの入力について

C言語ではscanf関数を用いましたが,以下のようにします。

s = input()

input関数は,どのようなものも入力できるため全ての場合に対応できる文字列として読み込まれます。 そのため,int型もしくはfloat型として読み込むには,以下のように型を指定して変換します。

s = input()
s = int(s) # int型
s = float(s) # float型

""" 直接読み込む場合"""
s = int(input())
s = float(input())

Pythonでは初期化の際に型が決まるので,数値なのか文字列なのかを先に指定する必要はありませんが,何の指示もないとその後不便です。 scanfには文字が書けなかったので,printf関数が必要でしたが,Pythonでは以下のように出来ます。

s = input("sに数値を入力してください。s = ")

課題2

整数 m が入力されると、それが奇数か偶数かを判定するプログラムを作成せよ.

(注:剰余を求める演算子 % を利用. これは整数型変数m, n に対し、m%n は m を n で割ったときの余りを与える.この演算子は整数型変数にのみ利用可能.)

課題3

2つの実数 s, t を入力すると, s > t, s = t, s < t か判定して表示するプログラムを作成せよ.(If-else文のパターン3を参考)

課題4

3つの実数, a, b, c を入力すると, x に関する2次方程式, ax2 + bx + c = 0 が実数解を持つか判定し, 表示するプログラムを作成せよ.

課題5

入力された整数が、3 の倍数か 7 の倍数であれば、その数値を表示するプログラムを作成せよ.( or を使う。)

課題6

“3 の倍数か 7 の倍数”である三桁の自然数の入力を促し、入力された数値が 条件を満たしているかどうか画面に出力するプログラムを作成せよ.(if文は1回しか使えないものとする))


for文・while文

次に重要なfor文についてです。これはC言語とかなり異なります。 C言語のfor文では初期化式・条件式・反復式の三つがありました。 Pythonでは、0から9まで出力するには以下のようになります。

for i in range(10): 
    print(i)

とするだけで繰り返しが実行されます。 このrangeというものはいくつか使い方があり、このように一つだけ数字を入れると、0からその数-1まで、繰り返していきます。

N回繰り返して計算したい場合には、以下のようになります。

N = 100
for i in range(N):
    print(i)

rangeには次の様な使い方があります。

N = 100
for i in range(1, N, 3):
    print(i)

これを実行すると1, 4, 7, …, 97という数字が出力されるようになります。 何をしているかというと,rangeの引数にNの前に1,後に3が加わることで,まず初めの1がfor文のスタート地点,最後の3が増加する量を指定します。 C言語で考えると

int i;
int N = 100;
for (i=0; i<N; i+=3){
    printf("%d\n", i);
}

という風になります。もし増加量が1で良ければ最後に数値を入れる必要がなく,以下の二つのfor文は同じものになります。

N = 100
for i in range(3, N): 
    print(i)
for i in range(3, N, 1): 
    print(i)

while文はインデントで制御文の範囲を指定する以外は、C言語に近いです。

例えば、

i = 0
while i < 20:
    i = i + 1
    print(i)

さて,ここまでに駆け足に基礎的な部分を学習してきました。

これだけでは機能が少なくPythonを学ぶありがたみが少ないです。 Pythonを利用するありがたみは、その豊富なライブラリにあります。 (もちろん他にもありますが。。。) 本講義で主に用いる、NumPyとMatplotlibというライブラリに使用方法を学び、実際に問題を解いてPythonに慣れていきたいと思います。

それでは,例題に取り組んでいきましょう。

C言語でも取り組んだ和と積を求めるプログラムは以下のようになります。

wa = 0
seki = 1
for i in range(1,5): 
    wa = wa + i 
    seki = seki * i
    print("和=", wa)
    print("積=", seki)

課題7

和と積のプログラムを改良して, 整数 n を入力すると 1 ~ n までの和と積を表示するプログラムを作成せよ. (注意:あまり大きな n を入力すると、特に積の値がコンピューターで扱える範囲を超えてしまうので、正確な値がでてこなくなります. それほど大きくない n で試してください.)

課題8

実数 a, 自然数 n を入力すると an を計算し表示するプログラムを作成せよ. (注意:あまり大きな n を入力すると、特に積の値がコンピューターで扱える範囲を超えてしまうので、正確な値がでてこなくなります. それほど大きくない n で試してください.)

課題9

実数 a, 自然数 n を入力すると a + a2 + a3 + … + an を計算し表示するプログラムを作成せよ.

課題10

自然数 n を入力すると n 以下の 3 の倍数と 7 の倍数を表示するプログラムを作成せよ.(for 文と if 文を使う. )

課題11

自然数 n を入力すると n が素数がどうか判定するプログラムを作成せよ.

以下,難しめの問題

課題12

三桁以下の(10進数の)自然数を入力したら,その数を二進数で表示するプログラムを作成せよ(for文を使って) 例: 三桁以下の自然数を入力してください:231 その数を二進数であらわすと11100111です.

課題13

7のn乗をAnとあらわすとする.下三桁が(943)となる最小のAnのnを求めよ. (追記:nが存在しない場合は,そのように出力せよ.) (intで扱える整数は,-2147483648から2147483647であることに注意.変数の値が2147483647を超えないように工夫しつつ,下三桁の値を調べていくこと.)

課題14

整数Mのn乗をMnとあらわすとする.三桁以下の整数Mの入力を促し下二桁が(16)となる最小のMnのnを求めよ. また,そのようなnが存在しない場合は,「そのようなnは存在しません」と出力すること.

課題15

xy平面上の曲線1:y=x*x と 曲線2:y=3+xはx>0の範囲で交点を一つもつ. この交点のx座標を近似的に求めるプログラムを作成せよ. ただし,正しい解とのずれは0.01以下であることが好ましい. (ヒント:求め方は自由だが,xを少しずつ増やしていき,曲線1,曲線2の上下が入れ替わる場所を近似的に見つければ良い)


NumPy:数学関数、ベクトルなどの利用

C言語では様々な数学関数を用いるためにmath.hをインクルードしましたが、 それに対応するライブラリは,numpyとなり以下のように読み込みます。

import numpy as np

C言語で#includeに対応するものがimportになります。そのまま後ろにas npとありますが,詳細は触れませんがプログラムの中ではこのnpというものを多用します。 importの後ろに書くものは,一般的にライブラリではなくモジュールと呼ばれます。特にNumPyはNumericalのNumから名前の由来のある数値計算用のモジュールで,これがあればかなりのことは出来ます。

数学関数・定数の利用

数学関数・定数などは以下のようにして使えます。

np.sin(x)
np.exp(-2*t)
np.pi

上記のxやtなどの変数は、1個のスカラーでも良いですし、リストでも構いません。 np.piはπを表します。

ベクトル(Array)の利用

リストのところで触れたとおり,この演習では配列に当たるものはこのarrayを利用していきます。まず一番簡単な利用方法は以下です。

X = np.array([1, 3, 5, 7])

このarray型のXの値を使いたい場合は,リストと同じようにx[0]などという風に利用できます。

さて,高々数個の配列を作るだけならこれでいいですが,N点ある配列が必要になる場合に,手打ちで入力していては埒が明きません。 C言語では,宣言した後for文で初期化していましたが,ここではNumPyにある関数を利用して,一気に初期化してarray型の配列を作ります。

その方法が以下です。

N = 200
X = np.zeros(N)

これで値が0のN個要素を持つ一次元配列を作ることができ,このXをC言語と同じようにfor文で初期化していくことが出来ます。

例えば,刻み幅がdtが1/Nとなる時間を表すものとし,tにNステップまでの時間を入れておこうとすると以下のようになります。

N = 100
t = np.zeros(N+1)
dt = 1 / N
for i in range(N+1):
    t[i] = i * dt

これで初期化が出来るのですが,これはC言語と同じやり方になります。 しかし,Pythonでは,もっと簡略した方法があります。

N = 100
Tstart = 0
Tend = 1
t = np.linspace(Tstart, Tend, N+1)

np.linspaceでは,一つ目の引数にスタートの数値,二つ目に終わりの数値,三つ目にその区間を分割する数を入力します。すると,for文を使わなくてもいっぺんに時間の情報を持った配列を準備することが出来ます。

また前に戻りますが,int型で配列を作る場合には,

N = 100
Tstart = 0
Increment = 3
ns1 = np.arange(Tstart, N)
ns2 = np.arange(Tstart, N, Increment)

np.arangeを使えば同様に出来ます。この場合,最後のインクリメントする数を指定しなければ1と自動で指定されます。


Matplotlib:グラフの描画(次回ちゃんと説明します)

MatplotlibはPythonを利用する大きな理由の一つになる、大変有名な可視化用モジュールになります。NumPyと同じようにモジュールを以下のように読み込みます。

それでは早速単純なsin関数を描いてみたいと思います。

import matplotlib.pyplot as plt
import numpy as np
N = 100
dx = 2.0*np.pi / N
x = np.zeros(N)
y = np.zeros(N)
for i in range(N): 
    x[i] = i*dx 
    y[i] = np.sin(x[i])
plt.figure(figsize=(10,8))
plt.plot(x, y)
plt.show()

(Jupyter Notebookで使う場合は、おまじないとして先頭に

%matplotlib inline

を書いてください。)

色々とまだ説明していないコードも書いてありますが,実行してもらうとsin関数が現れてくれると思います。

5行目のnp.piは見ての通りπの数値を与えてくれます。 12行目にあるplt.figure()はそれ自身で,グラフを描画する土台を作ります。実は省略できますが,さまざまな値がデフォルト値のままになるので一般的に利用して各種の値を指定します。figsize=(10,8)と指定することで,グラフを描く前のグラフの大きさを指定します。

最後のplt.show()は実際に目で見るために必要な呼び出しになります。

C言語とgnuplotの組み合わせとの大きな違いは,一つ一つの要素を書き出してグラフ化していたのですが,Pythonでは配列に全てのデータを入れてからグラフを書く形式が主流になります。

次回は,より多くの例題と,いろいろなMatplotlibによる描画を学びます。


関数

関数を利用者が自ら定義して利用することは,どのプログラミング言語でも必要な基礎的な技術です。早速Pythonにおける関数がどう定義されるかを見てみましょう。 まず,最も基礎的な入力された変数を書き出す関数です。

def myfunc(a): 
    print(a)

myfunc("hello")

returnがないのでC言語のvoid型の関数になります。しかしどこにもvoid型などと宣言するところがありません。 これは,変数の宣言がないのと同様に実際にreturnされる値が書き込まれるまでPythonでは関数が返す変数の型は定義されないので,変数と同様に必要がありません。 そして,:(コロン)で改行した後にはif文やfor文と同じように,インデントされています。これは理由も同じで,インデントされた部分だけが関数の中で処理される文と認識されるためです。 次に二つの和を返す関数を考えます。

def my_sum(a, b): 
    c = a + b 
    return c 
a = 1
b = 2
ans1 = my_sum(a, b)
ans2 = my_sum(3, -5)
ans3 = my_sum(2.0, 4.1)
print(ans1, ans2, ans3)

このようにするとaとbの値が入力されると,その二つの変数の和をとって返してくれる訳ですが,引数にも型の定義がないため,実際に入力される変数の型をそのまま利用して和を取ることになります。そのため,ans1のような使い方に加えて,ans3のようにfloat型の値を直接入力することが出来ます。

注意点としては,C言語と違い先に関数を宣言しておくプロトタイプ宣言ができません。基本的に宣言という概念がないので当然ではあるのですが,常に利用するよりも前に関数を定義しておく必要があるので注意してください。


確認問題

最終的に以下の問題を解けるようになってから,演習を進めていけるようにしたいと思います。

問題1

100以下の奇数のうち3の倍数でも7の倍数でも無いものを全て求め、 それらを画面に表示し、かつ、それらの和も画面に表示するプログラムを作成せよ。

問題2

配列変数を利用して、二つの10次元ベクトルuv の内積を求めるプログラムを作成せよ。

問題3

次の常微分方程式の初期値問題

$$ \frac{dy}{dt}=-y+\sin(t) , y(0) =1$$
をオイラー法を用いて数値的に解いてグラフ化せよ。
(時刻 $0 \leq t \leq 20$の範囲を20000等分割、50ステップ毎に値を表示)

昨年度の計算数学演習のホームページなど参照し、適宜復習してください。 (この演習では、上記問題程度のプログラミングはできるものとして演習を行います。多少の復習はしますが。)


計算数理A演習第3・4回

第3・4回(第2週)

今週の目標

  • グラフを描画する
  • アニメーションを描画する
  • 簡単な常微分方程式を解いて描画する

【重要】グラフを別ウィンドウで描画するためのSpyderの設定

特に何も設定していない状態でグラフなどを描画すると、iPythonコンソールの中に描画されます。これでは少々(演習の進行上)勝手が悪いので、別ウィンドウで開くように設定を変えます。

  1. 上部にある「ツール」から「設定」を選ぶ
  2. 左側の一覧から「iPythonコンソール」を選ぶ
  3. 右側上部のタブから「グラフィクス」を選ぶ
  4. 右側の「グラフィックのバックエンド」の中の「バックエンド」の項目を「インライン」から「自動」に変更
  5. すぐ下の「OK」
  6. spyderを再起動

Jupyter Notebookを使う人は、このページの最後にある付録を参照してください。


matplotlibによるグラフ描画

先週の演習では基本的なsin関数の描画を行い、まず描画するということになれてもらいました。 今週はmatplotlibで描画する際に必要になる基礎的な知識について学んでいきます。 少しハードルが高いですが、一番大事な情報はmatplotlibのホームページ(matplotlib.org)に書いてありますので、より高度なことをしたくなったら見てみてください。 また、実例を見てみることに勝る教科書はありませんので、どんなグラフがかけるのか興味があれば見てみてください。(https://matplotlib.org/gallery/index.html)サンプルコードもついているので、そのまま実行すれば手元で遊ぶことが出来ます。

matplotlibの構造

それでは、まず初めにmatplotlibの描画システムを理解するための基本構造を学びます。 下に示すのは公式ガイドに載っているmatplotlibの描画システムを説明するためのグラフです。

引用:https://matplotlib.org/1.5.1/faq/usage_faq.html

実際にはより複雑なシステムですが、ここでは最も頻度が高く重要な要素に絞って学びます。

※本当は全てオブジェクトと呼ばれていますが、本演習ではオブジェクト指向という考え方について深くは触れません。ひとまずグラフを描いたのちに、グラフ描画の2つの流儀について述べていますので、そちらを参考にしてください。何を言っているのか分からなくても、本演習の進め方には対して影響しませんので、構いません。

早速、コンソール上で以下のようにコマンドを入力してください。

import matplotlib.pyplot as plt
fig = plt.figure()

すると以下のような画面になったはずです。

左上にあるような何も描かれていない真っ白なウィンドウが現れたと思います。これがいわばキャンバスであり、Figureの実体になります。 本物のキャンバスは布と木で作られますが、それらがあったとして、いきなりキャンバスをもし作れと言われたら皆さんすぐに作れるでしょうか?絵を描くためのキャンバスには大小様々なものがあり、もし作れと言われたら当然サイズを指定する必要があります。 このFigureという構造も同様です。もちろん何の指示を与えなくてもデフォルトの大きさは与えられていますが。 ここでは示しませんが手元で、次のコマンドを実行してみてください。先のコマンドに比べるとかなり大きなウィンドウが現れたと思います。

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(10,8))

次に、その内側にある構造がAxesについてです。これは、データをグラフにするための座標系に当たります。早速以下のコマンドを実行してみてください。

import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10,8))

ax = fig.add_subplot(111)

見ての通り真っ白だったウィンドウの中に四角い枠と縦軸、横軸に[0,1]区間の目盛が加えられました。準備されたFigureに座標系を与えることで、数値を図示することが出来るわけです。実はFigureというキャンバスの中には複数の座標系を用意したり、他の字を描くための構造を書くことが出来ます。

※メモ
fig.add_subplot(111)の引数の意味は、100の位が縦方向のグラフの分割数、10の位が横方向のグラフの分割数で、1の位がその何番目かを表します。たとえば、fig.add_subplot(211)とすれば、上下に2分割されたうちの上半分にグラフが現れます。詳細はこの下の「複数のグラフを一つの画面に描画する」にあります。

 最後に緑色の円で囲まれているのが、axisという構造です。これは、Axesのそれぞれの軸を示しています。AxesはAxisの複数形で、先にも座標系と書きましたが、二次元か三次元のグラフしか描画しないmatplotlibでは、2つか3つの軸が必要になるので座標系に当たる構造がAxesとなるわけです。 グラフを描く際には、軸の幅や範囲など様々な情報を指定することが必要になります。そのときに最もよく利用して制御する必要がある構造が、このAxisになるわけです。

次にサンプルのための三角関数の数値を準備して、グラフを描くために以下のコマンドを入力してください

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)

x = np.linspace(0, 2.0*np.pi, 101)
y = np.sin(x)
ax.plot(x, y)

準備されたFigureに座標系を与えることで、数値を図示することが出来るわけです。実はFigureというキャンバスの中には複数の座標系を用意したり、他の字を描くための構造を書くことが出来ます。


グラフ描画の2つの流儀:オブジェクト指向インターフェース vs. pyplotインターフェース

次の2つのプログラムを実行して比較してみましょう。

プログラム1(オブジェクト指向インターフェース)
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
x = np.linspace(0,2*np.pi,101)
y = np.sin(x)
ax.plot(x,y, label="sin(x)")
ax.set_xlabel("x", fontsize=20)
ax.set_ylabel("y", fontsize=20)
ax.legend(loc="best", fontsize=20)
プログラム2(pyplotインターフェース)
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0,2*np.pi,101)
y = np.sin(x)
plt.plot(x, y, label="sin(x)")
plt.xlabel("x", fontsize=20)
plt.ylabel("y", fontsize=20)
plt.legend(loc="best", fontsize=20)

実行してみると、どちらも同じsin波が現れたと思います。 2つのプログラムを比べると、プログラム1に対してプログラム2では大きな違いは2点です。

まずプログラム1では、

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
ax.plot(x, y)

のようにfigやaxというインスタンスを定義し、それらを利用してplotや設定を行います。一方プログラム2では、すべてpltを利用しており、上で説明したようなオブジェクトを有効に利用していません。

また、plt.figure(および第2回で使ったplt.show)というコマンドを消しましたが、問題なくグラフが出てきたと思います。 これはplt.plotという関数が、ひとたび呼び出されれば、細かいことを気にしない代わりに基本設定のまま、空気を読んでグラフを描いてくれる関数だからです。 これによって、plt.figureやax.add_subplotなしにデータをすぐに可視化してくれるわけです。

いずれの方法でも構いませんが、これらのプログラムで描かれたグラフには、自然科学の実験やデータ解析で必要な必要最低限の情報が記載されています。今後レポート課題等はこのように必要な情報が載ったグラフの提出を心がけてくださいなお,上記プログラム中の”legend”関数についてはこちらのサイトに詳しく紹介しています.

小ネタ:オブジェクト指向?pyplot?

matplotlibとは、Matlabという科学技術計算ソフトの描画システムを真似て作られたもの、と言われています。今回の例の場合、Matlabでは、plt.〇〇の「plt.」を取り除けばそのまま使えるほど、使い方は近いです。ところで最近では、オブジェクト指向が主流になりつつあり、Matlabでもオブジェクト指向での記述が標準仕様になりました(現状は、従来式でも可)。グラフの詳細設定に関しては、オブジェクト指向インターフェースの方が、従来式より有用であることは、MatlabでもPythonでも同様です。おそらく今後は、様々なプログラミングの場面において、オブジェクト指向主義が完全な主流になります。早い段階からオブジェクト指向プログラミングを身につけていくと良いでしょう。


課題1

上のサンプルコードを書き換え、媒介変数表示(x(θ), y(θ)) = (cosθ, sinθ)、0<θ<2πとして、円を描くプログラムをかけ。


オブジェクト指向インターフェースによる描画の詳細設定

このやり方は、先のmatplotlibの構造を説明を行うときに用いた方法で、このやり方がmatplotlibの基本的なグラフ描画方法だと考えてください。 大まかな流れは、先ほど説明したようにFigureを作り、座標系となるAxesを準備して、データを描くことになります。

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
x = np.linspace(0, 2.0*np.pi, 101)
y = np.sin(x)
ax.plot(x, y, label="sin(x)")
ax.set_xlabel("x", fontsize=20)
ax.set_ylabel("y", fontsize=20)
ax.legend(loc="best", fontsize=20)
plt.show()

線の色・線のパターンの変更

matplotlibではデフォルトのグラフの色として、青色が選択され、線で結ばれてグラフが描画されます。 ここでは、任意の色に変更したり、点線にしたりしてみます。 変更方法はとても簡単で、上のサンプルコードのax.plotに以下のように追加するだけで変更できます。

ax.plot(x, y, "r--", label="sin(x)")

この例にあるようにしてもう一度グラフを描くと、赤い点線でグラフが描かれたと思います。matplotlibのplot関数では、データとなる二つの配列を与えた後に、文字列を書き込むことでグラフの色と線の種類を選択することが出来ます。 色は次に示す省略した文字で指定し、線の種類は記号の組み合わせで指定します。 基本的には、”[色][線の種類]”という構造になっています。 一文字のアルファベットで指定できる色には、 ‘b’(青), ‘g’(緑), ‘r’(赤), ‘c’(シアン), ‘m’(マゼンダ), ‘y’(黄), ‘k’(黒), ‘w’(白) があります。

次に線の種類を変更するには、一文字のアルファベットのあとに記号の組み合わせで指定できるのですが、簡単に指定できるものとして  ’-‘, ‘–‘, ‘-.’, ‘:’ があります。それぞれ試してみて線の種類を見てみてください。 (https://matplotlib.org/gallery/lines_bars_and_markers/line_styles_reference.html

色も種類もより高度な方法を用いれば細かな設定が可能ですが、ここではあまり高度な内容は必要ないので、興味がある人はマニュアルページを見てみてください。

参考: Specifying Colors (https://matplotlib.org/tutorials/colors/colors.html#sphx-glr-tutorials-colors-colors-py) Linestyles (https://matplotlib.org/gallery/lines_bars_and_markers/linestyles.html)

マーカーのプロット

線の種類はデータ間をつないで可視化しました。ここでは、そのデータ点自体を描画する方法を学びます。数値計算ではあまり表だって使うことはないかもしれませんが。

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
x = np.linspace(0, 2.0*np.pi, 101)
y = np.sin(x)
ax.plot(x, y, "o", label="sin(x)")
ax.set_xlabel("x", fontsize=20)
ax.set_ylabel("y", fontsize=20)
ax.legend(loc="best", fontsize=20)
plt.show()

サンプルコードを実行すると先ほどまでつながれていた線が消えて、データとしてxとyにあるデータ自体が点として描かれています。x軸方向は等間隔でもy軸のデータが山の頂点に近づくにつれて詰まっているのがよくわかります。 ここでは、プロットするタイプとして”o”(アルファベットのオー)を指定しました。線の種類と同様に他にもプロットの種類があります。 種類がかなり多いので、リンク先のmatplotlibのマニュアルページを参照してください。(https://matplotlib.org/gallery/lines_bars_and_markers/marker_reference.html) また、これはデータ点自体のある場所にプロットしているだけなので、データ間をつなぐ線は独立に描くことが出来ます。

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
x = np.linspace(0, 2.0*np.pi, 101)
y = np.sin(x)
ax.plot(x, y, "o-", label="sin(x)")
ax.set_xlabel("x", fontsize=20)
ax.set_ylabel("y", fontsize=20)
ax.legend(loc="best", fontsize=20)
plt.show()

描画範囲の指定

それでは次にグラフを描く範囲の指定方法を学びます。 特に指定しなければ、サンプルコードにあるようにデータを全て描ける範囲を自動で取ってくれます。 ここでは、あえて描画する範囲を絞って見ます。

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
x = np.linspace(0, 2.0*np.pi, 101)
y = np.sin(x)
ax.plot(x, y, label="sin(x)")
ax.set_xlim([0,2.0*np.pi])
ax.set_ylim([-1.0,1.0])
ax.set_xlabel("x", fontsize=20)
ax.set_ylabel("y", fontsize=20)
ax.legend(loc="best", fontsize=20)
plt.show()

このサンプルコードでは、描画するサイン関数がぴったりはまるように範囲を指定しました。]

ax.set_xlim([0,2.0*np.pi])
ax.set_ylim([-1.0,1.0])

上ののコードがそれぞれax.set_xlim,ax.set_ylimが、x軸、y軸の範囲を制限しています。x軸y軸どちらを指定するかは見ての通りxlimかylimかの違いで使い方は全く同じです。 関数の引数に与えるリスト[a,b]でaが軸の下限値、bが上限値を表しています。 サンプルコードでは、元のコードにあった余白がなくなっていることがわかると思います。


課題2

先のサンプルコードにおいて、x=0, x=π近傍を拡大して直線的に見える様子を確認せよ。


図形を描く

円を描く

図形を描くことはいろいろな場面で必要になるかと思いますが、matplotlibには基本的な図形を描けるようにモジュールが用意されています。これを使えば色々と図形を描くことが簡単にできるのですが、簡単な円を描くだけであればplot関数を用いて出来ますので、その方法を学びます。また多角形の枠の作り方も学びます。

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
cx = 0.0
cy = 0.0
ax.plot(cx, cy, 'o', markersize=200.0)
ax.set_xlim([-0.5, 0.5])
ax.set_ylim([-0.5, 0.5])
ax.set_xlabel("x",fontsize=20)
ax.set_ylabel("y")
ax.legend(loc="best")
plt.show()

これまでのplot関数に与えるデータはそれぞれいくつかの値を持つ配列だけでしたが、上のサンプルコードにあるようにそれぞれ一つのデータでもグラフを描くことができます。この場合データ間をつなぐ線の種類を指定しても一点しかデータがないのでつなぐことが出来ず、何も描かれません。そこで、データ点自体を描画するマーカーを指定してその座標点のデータを描画しています。ただし、折れ線グラフのようにデータをプロットしながら線を結ぶことを想定しているので、マーカーは線の太さよりも少し太いくらいです。なので、何かしら円を描くつもりでマーカー機能を使う場合には、サイズを大きくする必要があります。


課題3

ビリヤードのナインボールの初期配置のように9個の円で四角形を作れ。(向きは好きにしてください。)


多角形を描く

多角形は線をつなげばいいので、四角形は以下のようにすれば出来ます。

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
x = np.zeros(5)
y = np.zeros(5)
for i in range(5): 
    x[i] = np.cos((i*2.0*np.pi/4.0)+np.pi/4.0) 
    y[i] = np.sin((i*2.0*np.pi/4.0)+np.pi/4.0)
ax.plot(x, y, "-")
ax.set_xlabel("x", fontsize=20)
ax.set_ylabel("y", fontsize=20)
ax.axis('scaled')
ax.set_aspect('equal')
ax.legend(loc="best", fontsize=20)
plt.show()

matplotlibでは以下のようにいくつかの図形を簡単に描けるようになっています。 描くためのモジュールがpatchesで以下のサンプルコードにあるように必要な場合にインポートしてください。

import matplotlib.patches as patches
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)

# 円を作る
c = patches.Circle(xy=(0, 0), radius=0.5, fc='g', ec='r')
# 楕円を作る
e = patches.Ellipse(xy=(-0.25, 0), width=0.5, height=0.25, angle=10, fc='b', ec='y')
# 長方形を作る
r = patches.Rectangle(xy=(0.3, 0), width=0.25, height=0.5, angle=-10, fc='#00FFFF', ec='#00FFFF', fill=False)

# 作った図形を座標系の中へ描く
ax.add_patch(c)
ax.add_patch(e)
ax.add_patch(r)

# 二点の座標データを直接指定して直線を引く
ax.plot([0, 0.8], [0, -0.1])
ax.axis('scaled')
ax.set_aspect('equal')

実行するとこんな感じです。


課題4

多角形として、六角形と八角形を描け。可能であればn角形を描けるプログラムを作れ。


より良いグラフを作るために

任意の場所に文字を書く

次にグラフをよりよく説明するために必要な文章をグラフ内に書く方法を学びます。 ax.plot関数がデータを書き出すための関数でしたが、文字を描くにはax.text関数を使います。基本的なルールは、 ax.text(左下のx座標, 左下のy座標, [文字列]) のように文字を書き出す地点を指定して、次に書きたい文字列を書きます。

import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.text(3, 9, '1 Simplest way')
ax.plot(3, 9, 'o')
ax.text(3, 8, r'2 an equation: $E=mc^2$', fontsize=15)
ax.plot(3, 8, 'o')
ax.text(2, 6, '3 boxed italics text in data coords', style='italic')
ax.plot(2, 6, 'o')
ax.text(7, 3, r'4 unicode: Institut für Festkörperphysik', verticalalignment='bottom', horizontalalignment='right')
ax.plot(7, 3, 'o')
ax.text(0.55, 0.01, '5 colored text in axes coords', transform = ax.transAxes, color='green', fontsize=15)
ax.plot(0.55, 0.01, 'o')
ax.axis([0, 10, 0, 10])
plt.show()

 当然axは座標系を作るために呼び出したので、ax.textにまず初めに書き込む座標はこの座標系上での値になります。set_xlimなどを使い描画する範囲を指定するためには、text関数に入力する座標がその範囲に収まっている必要があります。 サンプルコードを順に見ていきましょう 1つ目の書き方が基本形です。特に指定することもないのであれば、これで十分です。 2つ目の書き方は、LaTeXコマンドを使う方法です。文字列は””か”で包めば文字列と認識されるのですが、その前にrとつけておくとLaTeXコマンドが利用できます。加えて、ラベルの指定と同様にfontsizeで文字の大きさを指定できます。 3つ目の書き方は、文字のフォントスタイルをstyle=’italic’とすることで斜体へ変更しています。 4つ目の書き方は、これまで座標位置が必ずテキストの左下だったものが、テキストの右下に変わっています。 これはverticalalignment=’bottom’, horizontalalignment=’right’によるもので、verticalalignmentには{‘center’, ‘top’, ‘bottom’, ‘baseline’, ‘center_baseline’}が指定でき、horizontalalignmentには{‘center’, ‘right’, ‘left’}が指定できます。 5つ目の書き方は、これまでのものと少し違います。上記の4つのtextでは、初めの座標がテキストの左下の位置と対応していることを見るためにax.plotで点を同じ座標に打っていて、ちゃんと左下角にあることが見て取れていると思います。しかし、最後のものだけ一致していません。 これは、後ろの方に書いてあるtransform=ax.transAxesというコマンドのためです。これを行うとaxによって作られた座標系の中の位置ではなく、figure全体に[0,1]×[0,1]の座標系があるような、絶対的な位置により指定されることになります。

グラフへの数値の描画

本演習を通して数値を表せる必要があるので、数値を文字列として書き出す方法を述べておきます。 いくつかやり方はあるのですが、format形式が私の知る限り一番便利なのでその方法を示します。 format形式では、以下のようにint型やfloat型の変数を文字列変換します。これを行うと表示だけでなく、ファイル名として保存するときにもパラメータの数字が入ったファイル名を連続して呼び出すことが出来ます。

import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
a = 2.3
b = 5
print(f'Format Style: a = {a}, b = {b}')
ax.text(3, 2, f'Format Style: a = {a}, b = {b}')

a = 2
b = 1
ax.text(a, b, f'Format Style: a = {a}, b = {b}')
ax.axis([0, 5, 0, 5])
plt.show()

これまでと違うのは文字列の”もしくは””の前にfを付け加えることと、文字列に変更したい変数を{}で囲うことです。 print関数で文字列を書き出す場合でも、ax.text関数でグラフに書き出す場合でも文字列として数値が出ているのがわかると思います。 format形式だと最後の例のように変数の値を変更すると数値が変更されていることがわかると思います。

小ネタ:print関数再訪

上の例で、format形式を学びました。その上で、以下の例を見てみましょう。

a = 1
b = 2
print('a = ', a, ', b = ', b, sep='')
print(f'a = {a}, b = {b}')

実行してみるとどうでしょうか?a = 1, b = 2が2回出てきたと思います。3行目は第2回で学んだやり方、4回目は今回学んだやり方ですが、結果に違いはありません。 では、この形式、つまり、

ax.text(3, 2, ['a = ', a, ', b = ', b])

と置き換えたらどうなったのでしょうか?(※ sep=”を含めるとエラーになるので、あらかじめ消しています。) print文で同じ結果だったので…?それとも…? 実際にどうなるか自分で確かめてみてください。

複数のグラフを一つの画面に描画する

複数のデータを一つのグラフ上にプロットするには以下のように二つの座標系を準備します。 fig.add_subplot(111)で一つの座標系をFigure全体に描画していました。ここでは書き込む座標系を二つにします。そのためにadd_subplotの引数について、以下のサンプルコードを読んでもらった後説明します。

fig = plt.figure(figsize=(10,8))
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)

x = np.linspace(0, 2.0*np.pi, 101)
y = np.sin(x)
y2 = np.cos(x)

ax1.plot(x, y, label="sin(x)")
ax1.plot(x, y2, '--', label="cos(x)")

ax1.set_xlabel("x", fontsize=20)
ax1.set_ylabel("y", fontsize=20)
ax1.legend(loc="best", fontsize=20)

ax2.plot(x, y2, '--', label="cos(x)")
ax2.set_xlabel("x", fontsize=20)
ax2.set_ylabel("y", fontsize=20)
ax2.legend(loc="best", fontsize=20)
plt.show()

fig.add_subplot(211)にある謎の211という数字の組み合わせですが、実はこの部分は以下のように書き換えることが出来ます。fig.add_subplot(2,1,1)

意味としては(行数,列数,場所)となりつながった三桁の数字も同じ意味です。 行数、列数によってFigureを縦横に必要な分だけ分割して、その中でどの領域に座標を設定するかを最後の数字で指定しています。分割された領域は行優先で数が割り振られています。 例えば、fig.add_subplot(3,4,10)という指示を出すと、ちょうど10の当りにだけ座標系が設定されます。 注意すべきは、分割した全ての領域を利用する必要はなく、あくまで作りたい座標系のサイズと位置を決めるだけです。なので、適当に与えるとグラフが重なってしまうので注意してください。

1234
5678
9101112

課題5

上下にグラフを分割して、上に任意の二次以上の多項式関数のグラフを描き、下段にその導関数のグラフを描け。定義域は0<x<3とする。ただし、導関数は数値的(近似的)に求めよ。


matplotlibでのアニメーション

この授業の範囲で手軽にアニメーションの描画を行う方法には、以下の方式で十分です。ただし、matplotlibのanimationの関数を使ったより高速な方法もあります。下の方に付録として記載しておくので、余力がある人は見てみてください。

この演習でメインに用いる方法は、一度描いたグラフのデータを更新することでアニメーションを作る方法です。題材として進行波を描くプログラムを用意したので実行してみてください。

import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
x = np.arange(0, 10, 0.1)
y = np.sin(x)
dt = 0.05
v = 1.0
lines, = ax.plot(x, y, "r")
for i in range(50): 
    t = dt * i 
    y = np.sin(x - v*t) 
    lines.set_data(x,y) 
    plt.pause(0.01)
plt.show()

グラフのデータが、line.set_data(x,y)の部分で更新されます。 グラフ自体の実態としてlineがまず、line, = ax.plot(x, y, “r”)によって作られています。ax.plot関数はグラフを描くと同時に、関数として描かれたグラフに関する様々な情報をreturnしてくれます。ここでは詳細に触れませんが、その中でlineとして受け取ったものをこの例題の様に制御するとグラフが変わることだけ覚えておいてください(気になる人は、変数エクスプローラーの歯車をクリックし、「Exclude unsupported data types」のチェックを外すと、linesの中身を見ることができます)。

このデータを更新するだけではグラフの描画は変更されないので、その次の行にあるplt.pause(0.01)によりグラフは更新されます。0.01は更新された後にそのまま何秒待つかを指定しています。変えてみてアニメーションの進む速度がどう変わるのか確かめてみてください。


練習

円がx軸上を0から10まで移動するアニメーションを作れ。

(ラインの更新方法については述べたが,patchの更新方法については紹介していない.
patches.Circleを使った方法で,中心座標を更新して描画する場合には,
変数名.set_center(xy=(x, y))で中心座標の更新ができる.)

例えば
c=patches.Circle・・・・・
と置いたのであれば,
c.set_center(xy=(x, y))
で中心座標の更新が可能.


課題6

いずれかの方法を用いて、中心座標に円を描きその周りを円が公転するアニメーションを作成せよ。

エクストラ課題1

課題6の公転する円を中心に,さらに一つ公転する円を描き(太陽,地球,月のような円を描き)アニメーションを作成せよ。その後,任意の円の数Nを入力すると,連鎖公転する円が描けるプログラムへ拡張せよ.ただし,各円の公転の角速度は(2π,4π,・・・2Nπ)となるものとする.(初期位相phaiはすべて0とし,公転軌道半径は任意のもので構わない.)

エクストラ課題2

エクストラ課題1で作成した”最後に生成される円”の軌道をアニメーション図中に併載させよ(軌道描画はアニメでなくても構わない)。この際,図の縦軸は”y”,図の横軸は”x”とする

エクストラ課題3

エクストラ課題2で描いた軌道および時間を基に,横軸が時間”t”,縦軸が”y”のアニメーション図(または,静止グラフ)を別途作成せよ。

エクストラ課題4

エクストラ課題3で,描いた軌道が近似的に矩形関数 – Wikipediaのような形にできるのか,またできるとしたら軌道半径・回転の角速度・初期位相をどのようにすれば良いか,考えよ。

エクストラ課題5

エクストラ課題4で矩形波を描けた原理を理解し,この動画を見て,連鎖公転円系を2軸分用意して任意の図形(ハートやミッキーなど)を2次元平面上に描画するプログラムを作成せよ(動画の2:40秒あたりに再生されているものを作る)。ただし,ハートを描くための関数はこちらを参照.(手順ヒント:①まずは2次元平面上に一筆書きの目標図形を描き,x座標用配列,・y座標用配列に値を格納する.②x座標用配列,・y座標用配列と同じサイズの時間t用配列を作成する.③得られた配列x(t), y(t)に対してそれぞれフーリエ級数展開を実装し,フーリエ係数を手に入れる.この際,同じ周波数成分のsinとcosのフーリエ係数から,位相差,および,ノルムを算出しておく.⑤得られた位相差・ノルム・周波数が公転系の何を意味するのかを考えてx軸用,y軸用の公転系をそれぞれ実装する.)




簡単な常微分方程式とその解のグラフ化

今日は、計算数理Aの講義に出てきた次の常微分方程式を用いて演習を進めます。この方程式は、講義で詳しく説明されたと思いますが、例えば放射性同位体の崩壊を記述するモデル方程式として導出されます。講義で紹介があったように、例えば放射性炭素による年代測定にはこういった微分方程式が役立ちます。$N(t)$を時刻$t$における放射性同位体の原子数とすると下記のような微分方程式が得られることは講義で見たとおりです。

(P)

\[\left\{
\begin{array}{l}
\frac{d}{dt} N(t) &= – \lambda N, \\
N(0) &= N_0
\end{array}\right.\]

さて、この微分方程式は簡単に解けて、解は次のようになることも講義で見ました。

(S) 
$$ N(t) = N_0e^{- \lambda t} $$

今日の目標は、解(S)をグラフとして表示するプログラムを作成することです。パラメータ$\lambda$および$N_0$を与えれば、(S)のグラフをかけるはずです。 実は前回の内容で$y= sin(x) $のグラフを描いてもらいましたが、前回の$y= sin(x) $を描く方法を記述した部分にしたがって、(S)のグラフを描いてください。


課題7

$ N(t) = N_0e^{- \lambda t} $のグラフを描け。ただし、$N_0 = 1, \ \lambda = 1,\ \ 0 \leq t \leq 10$とする。


課題8

課題7の式において、$N( \tau ) \ \ = \ \ 0.5N_0 $となる$\tau$(半減期といいます)を手計算で求めよ.ただし,$\ \lambda = 1$とする.

答え:$ \log 2$ (約0.693)


課題9

課題7で作成したプログラムを用いて課題8の$\tau$を近似的に次のような手法で求めたい。そのようなプログラムを作成せよ。

方法

$N(t)$の値を(S)を用いて飛び飛びの$t$の値$t_i \ \ = \ \ dt * i$ (ただし$dt$は時間刻み幅)について求めるのであるが、(S)と課題6のグラフから明らかなように$N(t)$は単調減少関数である。であるから、$N(t_i) \ \ \geq 0.5N_0 \geq N(t_{i+1})$となる$i$があるであろう。このとき、$t_i$を$\tau$の近似値$\tau^*$とする。$0 \ \ \leq t \ \ \leq 10$を100等分して上記方法にて$\tau$の近似値$\tau^*$を求め、課題8で求めた値との差(誤差と呼ぶ)$$err^* \ \ = \ \ | \tau-\tau^* | $$を求めよ。

注意:当然ながら刻み数を100から1000に増やせばより正確な値が求まる。

※参考:
課題9、課題10(後で出てきます)の方法は、N(t)が具体的に与えられていない場合 (例えば実測データなど)にも有効なので、実用的です。


課題10

課題9の方法ではあまり良い精度で近似値を求めることができない。 次の方法でより精度良く近似値をもとめることを考える。

$N(t_i) \ \ \geq 0.5N_0 \geq N(t_{i+1})$となる$i$を求め、2点$(t_i, N(t_i)), \ \ (t_{i+1}, N(t_{i+1}))$ を結ぶ直線を考える。 その直線と$N=0.5$の直線の交点の$t$の値$\tau^{**}$をもって、$\tau$の近似値とする。 (このような手法を線形補間と呼ぶ。)$0 \ \ \leq t \leq 10$を100等分して上記方法にて$\tau$の近似値$\tau^{**}$を求め課題8で求めた値との差$$err^{**} \ \ = \ \ | \tau – \tau^{**} |$$

を求めよ。課題9の結果よりも良いだろうか?

注意: 課題9の方法でも刻み数を増やせばより正確な値が求まるのであるが、課題10の補間を使えば、あまり多くない刻み数でも正確な近似値が得られることを実感してほしい。


課題11

課題10において刻み数100で近似値を求めた場合の誤差と同程度に小さい誤差とするためには課題9において刻み数をどの程度にしなければならないか。実験してみよ。


課題12(余力のある人はトライ!)

課題9および課題10の方法において、横軸刻み数・縦軸err(半減期の真の解と数値解との誤差の絶対値)とした図を描け。


付録:matplotlibのanimation関数を使ったアニメーションの描画

方法1:全ての図を変数に格納し、パラパラ漫画的に描画

次は一度描かれたグラフのデータを変更するのではなく、少しずつずらしたグラフを用意して、そのたくさんのグラフを一つの関数に渡すことでまさに「パラパラ漫画」の要領でアニメーションを作る方法です。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
fig = plt.figure()
ax = fig.add_subplot(111)
x = np.arange(0, 10, 0.1)
y = np.sin(x)
dt = 0.05
v = 1.0
ims = []
for i in range(50): 
    t = dt * i 
    y = np.sin(x - v*t) 
    im = ax.plot(x, y, "r") 
    ims.append(im)
ani = animation.ArtistAnimation(fig, ims)
plt.show()

演習で行なったやり方(以下、方法0と呼びます)では、ax.plot関数はfor文の外側で一度使っただけですが、今回はfor文の中で毎回呼び出します。そして、そのグラフに関する情報をimとして受け取ります。方法0ではlinesを作るときにlinesの後ろに”, “がついていたのですが、今回はimの後ろに”, “がついていないことに注意してください。 そして、for文の直前に書いてあるims = []というコードは、そのパラパラ漫画を格納するためのリストですが、最初は空のリストです。ims.append(im)によって実際に格納していき、for文が終了するとimsの中に50枚の絵が用意されたと思ってください。 最後に用意されたimsとアニメーションを書き出したいキャンバスであるfigを引数として持つanimation.ArtistAnimation関数に二つの変数を渡すことでアニメーションを作ることが出来ます。

方法2:変化を定義する関数を与える方法

方法1にかなり近いやり方ですが、少し違う方法を紹介します。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
fig = plt.figure()
ax = fig.add_subplot(111)
x = np.arange(0, 10, 0.1)
y = np.sin(x)
dt = 0.05
v = 1.0
line, = ax.plot(x, y)
def init(): # only required for blitting to give a clean slate. 
    line.set_ydata([np.nan] * len(x)) 
    return line, 
def animate(i): 
    line.set_ydata(np.sin(x + i / 100)) 
    # update the data. 
    return line,
ani = animation.FuncAnimation(fig, animate, init_func = init, frames=range(0,2*314,1), interval=1, blit=True, save_count=100)

plt.show()

この方法ではfor文を直接使わずに、animation.FuncAnimation関数にfor文で使われるコードをanimate関数として与えることでアニメーションを描いています。init_func関数で初期化し,animate関数でlineのy座標を更新・描画、framesがiに渡される引数でfor loop文の代わりの役目をしています.

どのやり方も一長一短なのですが、たぶん方法0が一番楽かと思います。 ただし、作ったアニメーションを動画形式で保存するには、方法1か方法2であると楽でしょう。

付録:Jupyter Notebookでアニメーションを見るための設定

※macOS Mojaveではこの方法は使えません!強制ログアウトされます! macOS Catalinaでは解決しているようですが…

これまでグラフを描画してjupyter上で見るために、初めに

%matplotlib inline

と書いていました。アニメーションを見るためには、この方法では見られないため、以下のように変更してください。

%matplotlib tk

この部分を変更して実行しても、すでに先のコマンドを実行してjupyter notebookを使っている場合には、変更がききません。 新しく二つ目のコマンドを有効にするためには、いったんnotebookを閉じるか、ツールバーのkernelを選択してRestartをクリックして再起動する必要があります。

計算数理A演習第5・6回

第5・6回(第3週)


グローバル変数とローカル変数

これまで関数と変数について習いました。ここでは、プログラム中での変数の定義の位置によって、変数の意味合いが変わることを見ていきましょう。 まずは基本となるプログラム(sample_global1.py)を考えましょう。

sample_global1.py

a = 10
print(f'a={a} in initial')

もはやこのプログラムの説明は不要でしょう。実行結果はa=10 in initialです。では次のような関数が含まれる場合(sample_global2.py)はどうでしょうか。

sample_global2.py

a = 10

def func1(): 
    print(f'a={a} in func1')

print(f'a={a} in initial')
func1()

実行してみると、

a=10 in initial
a=10 in func1

という結果が返ってきます。 計算数学演習でC言語を学習した人は「何故関数func1の中で、引数でもなく、定義もされていない変数aの値が得られるのだろうか?」という疑問を持つ人がいるかもしれません(疑問に思ってくれると助かります)。

※ C言語では、main関数で定義した変数の値を別の関数で使うには、引数として使うことを定義しなければならない、と教えました。一方、Pythonでは、このmain関数というものがありません(あるいは、1つのpythonプログラムそのものがmain関数という考え方もできますが)。

今回の変数aのように、関数の定義の外に定義している変数をグローバル変数といい、定義されている関数内でも参照することができます。では次のプログラム(sample_global3.py)はどうでしょうか?

sample_global3.py

a = 10

def func1(): 
    print(f'a={a} in func1') 
    a = 20

print(f'a={a} in initial')
func1()
print(f'a={a} in final')

もしグローバル変数である変数aを関数内で読み書きすることできるのであれば、

a=10 in initial
a=10 in func1
a=20 in final

という結果が返ってくることを期待するはずですが、残念ながらエラーが返ってくるはずです。 これは、「関数内で変数aの値の代入(書き換え)を伴っているため、変数aはグローバル変数ではない」と解釈されてしまうからです。このときの変数aは、ローカル変数といい、関数内でのみ利用可能な変数になってしまっています。そのため、sample_global3.pyのprint(f”a={a} in func1″)の段階で、「変数aが定義されていないのに使われているよ!」とエラーがでるわけです。実際、次のプログラム(sample_global4.py)を実行すると、関数内での処理が終わった後は、さも代入などなかったかのように元の値に戻っています(正確にはそもそも変わっていないのですが…)。

sample_global4.py

a = 10

def func1(): 
    a = 20 
    print(f'a={a} in func1')

print(f'a={a} in initial')
func1()
print(f'a={a} in final')
a=10 in initial
a=20 in func1
a=10 in final

では、グローバル変数として関数内で変数aの値を変更するにはどうすれば良いのでしょうか?例えば次のプログラムのように関数内で変数aがグローバル変数であることを明示すれば良いでしょう。

sample_global5.py

a = 10
def func1(): 
    global a 
    a = 20 
    print(f'a={a} in func1')
print(f'a={a} in initial')
func1()
print(f'a={a} in final')

global aによって、変数aがグローバル変数であることが宣言されています。これらのプログラムを図的に表すと次のようなイメージです。

このように、グローバル変数とローカル変数という考え方を上手く使うと、効率的なプログラムを書くことができます。例えば、今後行うシミュレーションにおいて、定数パラメータをグローバル変数で定義すれば、いちいち引数などを使わなくても、プログラム全体で使用することができます。一方、広い範囲で使える、ということは、どこかで変更すると、その変更が他の部分に渡って反映されてしまう、ということでもあります。この場合、プログラムがこちらの想定外のところで予想と反した挙動をとるかもしれませんので、使用には注意が必要です。また、sample_global4.pyのように、同じ名前のグローバル変数とローカル変数が混在するようなプログラムも極力避けたほうが良いでしょう。

ちなみにC言語においてもグローバル変数を定義することができます.ヘッダーファイルなどのincludeが終わった後(関数の定義の前)に変数を定義すれば,そのプログラム全体にわたって使うことができます.

常微分方程式の数値解法(オイラー法)

今回は次のような常微分方程式の初期値問題を、オイラー法と呼ばれる方法を用いて数値的に解いて結果をグラフにしてもらいます。 オイラー法は計算数学演習で扱っていますが、おさらいをしておきます。 次の問題を考えます。

\[
\left\{
\begin{array}{l}
\frac{dy}{dt} \ \ &= \ \ f(t,y)\\
y(0) \ \ &= \ \ a  
\end{array}
\right. \ \ \ \ \ \ \ \ (1)\]

求めたいのは、$t>0$の範囲の$y(t)$であり、関数$f(t,y)$および定数$a$は与えられているとします。 また、$f(t,y)$の値は$t \ \, \ \ y$が与えられればいつでも計算できるものとします。(関数として定義するとよい。)

コンピューターでは数値を離散的に(とびとびに)しか扱うことができません。 そこで、(1)式をコンピューターで扱えるようにする必要があります。 (1)式をコンピュータで解く方法には色々とありますが、今回は代表的なものの一つであるオイラー法を用います。 その前に、準備として差分商について説明します。

差分商と差分方程式

$f(x)$を$x$の関数としたとき、$f(x)$の$x=a$における微分係数は$$\lim_{h \to 0} \frac{1}{h} \{f(a+h) \ \ – \ \ f(a) \} \ \ \ \ \ \ (2)$$
で定義されます。ここで、定義中の極限操作を取り払い、$h$を有限にとどめた
$$D \ \ = \ \ \frac{1}{h} \{f(a+h) \ \ – \ \ f(a) \} \ \ \ \ \ \ (3)$$
を考えると、$h$を十分小さな正の実数ととれば、(3)式は(2)式の近似となっていると考えられます。 この$D$のように、関数のいくつかの点における値の差を用いてその関数の微分係数を近似することを差分近似といい、(3)式の右辺のような量を差分商といいます。 いまの場合は1階微分係数を近似する1階差分商です。

では、このような差分商を用いて(1)式を離散化してみましょう。 まず、(1)式の第一式の微分係数を上記の差分商で置き換えてみます。
$$\frac{1}{h} \{y(t+h) \ \ – \ \ y(t) \} \ \ = \ \ f(t, y(t)) \ \ \ \ \ \ (4)$$

ただし、$h$は正の数とします。 (4)式と(1)式の第一式は異なる方程式なので、(1)式の第一式の解 y(t) は一般に(4)式を満たしません。 そこで、混乱を防ぐために(4)式の$y(t)$を$Y(t)$と書き換えましょう。

$$\frac{1}{h} \{Y(t+h) \ \ – \ \ Y(t) \} \ \ = \ \ f(t, Y(t)) \ \ \ \ \ \ (5)$$

5)式のように差分を含む方程式を差分方程式といいます。

 次に、(1)式の$y$を$Y$に置き換えた初期条件

$$Y(0)=a \ \ \ \ \ \ (6)$$

の下で(5)式を解くことを考えます。(5)式は、

$$Y(t+h)= Y(t)+hf(t,Y(t)) \ \ \ \ \ \ (7)$$

と変形できるので、$t=0$を代入すると、

$$Y(h) \ \ = \ \ Y(0)+hf(0,Y(0)) \ \ = \ \ a+hf(0,a)\ \ \ \ \ \ (8)$$

となり、$Y(0)$から直ちに$Y(h)$が求まります。同様にして(7)式を繰り返し用いると、

\[
\left\{
\begin{array}{}
Y(2h) \ \ &= \ \ Y(h)+ hf(h,Y(h)),\\
Y(3h) \ \ &= \ \ Y(2h)+ hf(2h,Y(2h)),\\
Y(4h) \ \ &= \ \ Y(3h)+ hf(3h,Y(3h)),\\
\vdots\\
Y((j+1)h) \ \ &= \ \ Y(jh)+ hf(jh,Y(jh)),\\
\vdots\\
\end{array}
\right. \ \ \ \ \ \ \ \ (9)
\]

というように、$j=1,2,3,…$ とすると$Y((j+1)h)$を$Y(jh)$から計算できることがわかります。

$h$をいったん決めると、$t=jh$以外の時刻の$Y$の値は(7)式からは求めることができません。 このように、差分方程式を解くと従属変数はとびとびの時刻で値が定まります。 そのようなとびとびの時刻を格子点と呼びます。 $Y$は格子点でのみ意味があるので、そのことを明示するために$Y(jh)$を$Y_j$と書き換え、$t_j=jh$とすると、(5)式と初期条件は、

\[
\left\{
\begin{array}{l}
\frac{1}{h}(Y_{j+1}-Y_j )\ \ &= \ \ f(t_j,Y_j),\\
Y_0 \ \ &= \ \ a
\end{array}
\right. \ \ \ \ \ \ \ \ (10)
\] 

となり、結局(1)式の常微分方程式の初期値問題が、$Y_j$に関する漸化式の問題(10)式に置き換えられました。この方法をオイラー法と呼びます。見やすいように(10)を書き直すと

\[
\left\{
\begin{array}{l}
Y_0 \ \ &= \ \ a,\\
Y_{j+1} \ \ &= \ \ Y_j+hf(t_j,Y_j)\\
\end{array}
\right. \ \ \ \ \ \ \ \ (11)
\]  

となります。

1階常微分方程式に対するオイラー法のアルゴリズム

解を求める時刻の上限を$T$とし、適当な自然数$N$を定めて$h= \frac{T}{N}$とします。

ちなみに宣言は必要ないですが,
T, h, a 及び 関数 f の返り値は実数型(float); N, j は整数型(int); Y[N+1], t[N+1] は実数型配列(float)です.

###########################################################################

T, N, aを設定

h = T/N 

配列Y[N+1], t[N+1]を用意する

問題文に沿って$ \frac{dy}{dt}$ の関数 f(t, y) を定義

Y[0] = a (初期条件の設定)

添え字 j = 0, 1, 2, …, N-1の順に(for文)
Y[j+1] = Y[j] + h * f(t[j], Y[j])
以上をj=N-1まで繰り返す。

matplotlibを使ってグラフを描く

##########################################################################

f(t,Y) は関数として定義するとよいでしょう。 ここでは配列を駆使したアルゴリズムの記法を紹介しておりますが,配列を極力用いない記法でもももちろんOKです。

簡単な常微分方程式とその数値解のグラフ化

さて、実際にオイラー法をつかって常微分方程式を解いてみましょう。 今回も、計算数理Aの講義に出てきた次の常微分方程式を用いて演習を進めます。(ただし講義では$N$としていた変数名を$y$と書き換えました。上記アルゴリズム等の分割数$N$とまぎらわしいので。)

\[
\left\{
\begin{array}{l}
\frac{d}{dt}y(t) \ \ &= \ \ – \lambda y,\\
y(0) \ \ &= \ \ y_0\\
\end{array}
\right. \ \ \ \ \ \ \ \ (P)
\]  

前回(第3・4回)は、この微分方程式を解析的に解いて得られた解をグラフ化してもらいました。今回は(P)式をオイラー法を用いて数値計算で解いて、得られた数値解をグラフ化してもらいます。


課題1

(P)をオイラー法で解き、得られた数値解をグラフとして表示するプログラムを作成せよ。ただし、$Y_0=1, \lambda =1, 0 \leq t \leq 10$とする。例えば次のようになる。 (この例では、$0 \leq t \leq 10$を100等分している。つまり時間刻み幅を h と書くことにすると、 h = 10.0/100 = 0.1 である。) 

先週、解析的に求めた解をグラフとして表示したものは以下であるが、殆ど違いがない事に注意。 (課題2でみるが、微妙に異なっている)


課題2

前回解析的に求めた解(S)とオイラー法で求めた数値解を重ねて描くプログラムを作成せよ。 例えば、次のようになる。 この例では、解(S)を青色で描き、数値解を赤色で描いている。 (この例では、0≦t≦10 を 20等分している)

刻み幅を小さくすると両者の差は縮まる。(次の例では、0≦t≦10 を 300等分している)


課題3

次の常微分方程式の初期値問題をオイラー法で解き、t と y の関係をグラフ化せよ。(次の小ネタも参照)

(1) dy/dt = -y + sin(t), y(0) = 1  (時刻 0≦ t ≦20 の範囲を20000等分割、100ステップ毎に値を表示)

(2) dy/dt = y(5-y), y(0) = 1  (時刻 0≦ t ≦20 の範囲を20000等分割、50ステップ毎に値を表示)

(3) dy/dt = y – 2y3, y(0) = 0.1 もしくは y(0) = -0.1 (時刻 0≦ t ≦20 の範囲を20000等分割、100ステップ毎に値を表示)

小ネタ:配列の要素を部分的に抜き出す

pythonの配列の便利さとして、次のような使い方ができます。x = np.linspace(0,1,101)[[0,4,6]] # 配列の0・4・6番目を抜き出すx[3:10:2] # 配列のうち3以上10未満の番号のものを2つ飛ばしで抜き出すx[:4] # 配列のうち4未満の番号のものを抜き出すx[4:] # 配列のうち4以上の番号のものを抜き出すx[4:-1] # 配列のうち4以上の番号でかつ最後でないものを抜き出す

これを使うと、必要以上にデータを描画したくないときに役に立ちます。


レポート課題

課題2で作成したプログラムを少し変更して、 「λの値の入力を促し、そのλの値における解析解とオイラー法で求めた数値解を重ねて表示する」 プログラムを〇〇.py形式(名前は自由)で作成し、Moodleから提出して下さい。 初期値、x、yの表示範囲は課題2と同じでかまいません。 0≦t≦10 を 100等分とすること。数字や軸の名前が描いてあるかどうかも評価のポイントです。関数を作ってmain関数をシンプルに(読み易く)記述しているものは高く評価します。 コメントは必ず行うこと(#以降はコメント文になります)。 また、プログラムの最初の方にコメントとして学生番号と氏名は(最低限)必ず記述すること。 (# bxxxxxx 氏名) 提出期限2022年5月5日(金)23時59分


自由課題(上記の課題が終わった人へ)

セルオートマトン

一次元セルオートマトンについて調べて描いてみてください。 ただし、ある時刻jのときの各セルiがもつ値x[i][j](0か1)は自分の値と両隣の値をもとに、次の時刻j+1の自分の値を決定する物とします。 (セルの数は自由に設定してください)

例としてRule90と呼ばれるもの以下に示します。 (iがセルの番号で、jが時刻を表すと思ってください)

多次元配列を使っても良いですし、1次元配列を使っても良いです。1のところだけ●をつけると、次のような図が得られるはずです。

今回のように、(自分自身と)両隣のセルの値に応じて次のステップの値が決まるようなものを1次元3近傍系のセルオートマトンと言います。取りうる値の組合せは、3つのセルが2状態(0 or 1)を取るので、2^{2^3}=256通りになります。それぞれの値の取り方によって、以下のような決まりによってRuleの番号が定められます。ちなみに藤井が個人的に好きなRuleは184です。(何故でしょう?)


講義で出てきた1階常微分方程式

いくつか講義で出てきた問題をあげておきます。 オイラー法で解いてみて、解をグラフとして表示してみてください。 初期値やパラメータを適当にきめて、いくつかシミュレーションしてみてください。

\[
\left\{
\begin{array}{l}
\frac{dx}{dt} \ \ &= \ \ \gamma x,\\
x(0) \ \ &= \ \ x_0\\
\end{array}
\right. \ \ \ \ \ \ \ \ (12)
\]  

\[
\left\{
\begin{array}{l}
\frac{du}{dt} \ \ &= \ \ u(1-u),\\
u(0) \ \ &= \ \ u_0>0\\
\end{array}
\right. \ \ \ \ \ \ \ \ (13)
\]  


2階常微分方程式の数値解法

2階常微分方程式もオイラー法を用いて解くことができます。 次の問題を考えて行きましょう。

\[
\left\{
\begin{array}{l}
y^{ \prime \prime} \ \ &= \ \ – \omega ^2 y,\\
y(0) \ \ &= \ \ 1,\\
y^{ \prime} (0) \ \ &= \ \ 0\\
\end{array}
\right. \ \ \ \ \ \ \ \ (14)
\]  

講義の説明の通り(下の枠中)、これは単振動を記述する2階の常微分方程式です。 ただし簡単の為、時間$t$での微分を$^{\prime}$ を用いてあらわしています。 バネ定数$k$のバネの一端を固定し、他端に質量$m$のおもりをつけた状況を考えます。

このおもりを少しだけ右に引っ張ってから手を放します。 おもりの中心の時刻$t$におけるつり合いの位置からのずれを$y(t)$とすると、$y(t)$が満たす微分方程式は

$$\frac{d^2y}{dt^2} \ \ = \ \ – \omega ^2 y \ \ \ \ \ \ (15)$$

ただし、

$$ \omega \ \ = \ \ \sqrt{ k/m } \ \ \ \ \ \ (16)$$

としました。

2階の常微分方程式は次のように連立の1階の常微分方程式に帰着できます。すなわち、$y_1(t)=y(t), \ \ y_2(t)=y^{\prime}(t)$ とおくと、式(14)は、

\[
\left\{
\begin{array}{l}
y_1^{ \prime} \ \ &= \ \ y_2,\\
y_2^{ \prime} \ \ &= \ \ – \omega ^2 y_1,\\
y_1(0) \ \ &= \ \ 1,\\
y_2(0) \ \ &= \ \ 0\\
\end{array}
\right. \ \ \ \ \ \ \ \ (17)
\]  

 となります。 この形になれば、前回紹介したオイラー法を用いて解を求めることができます。 アルゴリズムは次のようになります。 但し、$T$を解を求める時間の上限、$h$を時間刻み幅、$N$を時間刻み数とします。 また、変数は$y_1, y_2$として、$a_1, a_2$を初期値とします。 また、$f_1(t, y_1, y_2) および$f_2(t, y_1, y_2)$  はそれぞれ関数で式(17)の場合には$f_1$が第一式の右辺、$f_2$が第二式の右辺となります。


2階常微分方程式に対するオイラー法のアルゴリズム

解を求める時刻の上限を T とし、適当な自然数 N を定めて h=T/N とする。
T, h, a1, a2, y1, y2, y1_new, y2_new, t 及び 関数 f1, f2 は実数型 ,N, j は整数型

#############################################################
T,N,h を定める
配列y1[N+1], y2[N+1], t[N+1]を用意する
f1(t,y1,y2), f2(t,y1,y2) を関数として定義
y1[0] = a1,
y2[0] = a2,
t[0] = 0
j = 0,1,2,….,N-1の順に
t[j+1] = (j+1)*h
y1[j+1] = y1[j] + h*f1(t[j], y1[j], y2[j])
y2[j+1] = y2[j] + h*f2(t[j], y1[j], y2[j])を繰り返す
matplotlibでグラフを描画
#############################################################

課題4

式(14)を解析的に解け。


課題5

$ \omega = 1$として式(17) をオイラー法で解き、グラフを描け。グラフは横軸が$t$であり、縦軸を$y(t)$とせよ。 また、時間刻み幅$h$を変えたときに結果がどのようにかわるか観察せよ。 0≦t≦20の範囲において、h=0.0001,0.001, 0.01, 0.1それぞれの場合について、解析解とどの程度あっているか各自調べよ。

色々な h に対する計算結果は次のようになる(青はh=0.1、オレンジはh=0.01、緑はh=0.001、赤はh=0.0001のときの結果。紫は解析解のグラフ)。 この結果から、h は十分小さい値でないとまずいことがよく分かる。

オイラー法では h を十分小さくとらないと本当の解をうまく近似できません。h を小さく取るということは、計算時間が多くかかることを意味します。そこでより高精度で大きな h でもうまく解を近似できるような解法が求められます。そのような要求に応える解法としては、ルンゲ・クッタ法があります。(計算数学演習でもやっていますが、次回改めて簡単に紹介します。)


課題6

課題5のプログラムを用いて、

$$E \ \ = \ \ \frac{1}{2}(y^{\prime})^2+\frac{1}{2}y^2 \ \ \ \ \ \ (18)$$

を計算せよ。 結果は、横軸を$t$、縦軸を$E$とするグラフで確認すること。 なお、この$E$は$m=k=1$のときの、バネの力学エネルギーに対応する。


課題7

実際のバネではバネの復元力以外におもりの速度$y^{\prime}$に比例する抵抗が働く。 その抵抗を考慮すると、式(14)ではなく次の式になる。($2 \gamma y^{\prime}$の項が新たに加わった。)

\[
\left\{
\begin{array}{r}
y^{ \prime \prime} + 2 \gamma y^{ \prime} + \omega^2y\ \ &= \ \ 0\\
y(0) \ \ &= \ \ 1\\
y^{\prime}(0) \ \ &= \ \ 0 \\
\end{array}
\right. \ \ \ \ \ \ \ \ (19)
\]  

ここで$\gamma$は抵抗力の強さを表す正の定数です。 式(19)の問題を連立1階常微分方程式に変換し、オイラー法を用いて解き、下のように「左端から線の繋がった赤い質点が振動する」アニメーションを作成せよ。

ただし、$ \omega = 2, \gamma=0.2$とし、時間の刻み幅$dt$は、0.01とする。また、左上の”t= …”は現在の$t$を表示している。 デモプログラムでは、時間の刻み幅は$dt=0.01$として、$t=50$まで計算している。

今回は初めて微分方程式を解いてアニメーションを作成する、ということをやります。前回の資料も参考にしつつ、可能な限り自力で頑張ってみてください。どうしても無理!という人は、以下のリンク先に解答例を置いておきますので、参考にしてみてください(文字化けする場合は、テキストエンコーディングをUnicodeにするか、ダウンロードしてエディタで見てください)。

解答例1:逐次描画する場合(推奨、簡単)
解答例2:animation.ArtistAnimation関数を使う方法(ややコツが必要)
解答例3:animation.FuncAnimation関数を使う方法(トリッキーさが必要)

今後もこのようなアニメーションを作成するプログラムを作る機会が増え、レポートでも提出してもらうことが増えます。提出するプログラムでは、アニメーションの描画を$t$が$0.1(=10dt)$動く毎におこなうようにしてください。(ヒント1: “%”をつかったif文) これは、無駄にプログラムの実行時間が長くなるのを防ぐためです。

“t=”の部分のアニメーションのさせ方: 他の点や棒の表記と同様にオブジェクトとしてテキストを作り、それをset_text関数を使って更新していきます。簡単な例は以下のようになります。 注意点はax.plotではline, のようにカンマをつけていましたが、textにはいりませんのでよくよく注意してください。

%matplotlib tk #必要な人だけ書く,基本的に書かない.
import numpy
import matplotlib.pyplot as plt
T = 100
h = 0.01
fig = plt.figure()
ax = fig.add_subplot(111)
t = 0.0
txt = ax.text(0.3, 0.5, f"t={t:.2f}", transform=ax.transAxes)
for i in range(T): 
    t = i*h 
    txt.set_text(f"t={t:.2f}") 
    plt.pause(0.01)

課題8

下図のような重力下で棒の長さ$L$が一定の振り子運動について考える。振り子の角度(ラジアン)を$X$とすると$X$がある程度小さければ、$X$は

$$ LX^{\prime \prime} \ \ = \ \ -gX \ \ \ \ \ \ (20)$$

という運動方程式を満たす($g$は重力加速度、摩擦は無視しています)。 $L=1, g=9.8$として、この方程式を適当な初期条件、$X(0)=0, X^{\prime}(0)=a$でシミュレーションし振り子の動画を作成せよ。

今回の課題は、オイラー法では十分に小さい$h$を用いないと計算結果がすぐにずれてしまいますが、練習としてやってみてください。

次回、オイラー法より計算精度の高いルンゲ・クッタ法を紹介しますが、そのときに比較してみてください。


課題9

課題8の振り子が2つ相互作用する場合を考える. 例えば, 2つの振り子の角度を$2 \pi X_1, 2 \pi X_2$とすると、$X_1, X_2$が

\[
\left\{
\begin{array}{r}
L_1 X_1^{ \prime \prime} \ \ &= \ \ -g sin( 2 \pi X_1) -C sin( 2 \pi (X_1 -X_2) ) \\
L_2 X_2^{ \prime \prime} \ \ &= \ \ -g sin( 2 \pi X_2) -C sin( 2 \pi (X_2 -X_1) ) \\
\end{array}
\right. \ \ \ \ \ \ \ \ (21)
\]  

という運動方程式を満たすとする。 この方程式を適当な $L_1, L_2, C$と初期条件、$X_1(0), X_1^{\prime}(0), X_2(0), X_2^{\prime}(0)$でシミュレーションし、振り子のアニメーションを作成せよ. また振り子の数をもっと多くした場合も考察せよ。

計算数理A演習第7・8回

第7・8回(第4週)

ルンゲ・クッタ法

前回まではオイラー法を用いた数値解法の演習を行いました。
今回は計算数学演習でも学んだルンゲ・クッタ法を用いて、次のような常微分方程式の初期値問題を数値的に解き、結果をグラフにしてもらいます。

\[
\left\{
\begin{array}{l}
\frac{dy}{dt} \ \ &= \ \ f(x,y)\\
y(0) \ \ &= \ \ a
\end{array}
\right. \ \ \ \ \ \ (1)
\]

プログラミング言語は違えど、アルゴリズムは同じですので、ルンゲ・クッタ法についての説明は省略します。計算数学演習を履修していない、あるいは、履修したはずだけど忘れてしまった、という人は、計算数学演習のページを確認するか、このページの最後の方に付録として載せておきますので、勉強しておいてください。

以下の課題では、4次のルンゲ・クッタ法で実装して下さい。


課題1

上記のルンゲ・クッタ法を用いて次の問題を解き、ルンゲ・クッタ法がオイラー法と比べて、どの程度性能が良いか比べよ。
次の初期値問題を$0 \leq t \leq 5$の範囲でh=0.5とし、実際にオイラー法とルンゲ・クッタ法を用いて解き、結果を比較せよ。

\[
\left\{
\begin{array}{}
\frac{dy}{dt} \ \ &= \ \ \lambda y\\
y(0) \ \ &= \ \ 1 \\
\lambda \ \ &= \ \ 1
\end{array}
\right. \ \ \ \ \ \ (2)
\]


課題2

ルンゲ・クッタ法を用いて、前回の課題7と同等のアニメーションを表示するプログラムを作成せよ。$ \gamma = 0, dt = 0.1$
として、オイラー法との違いを確認すること。


振動現象について

常微分方程式はオイラー法とルンゲ・クッタ法の二種類の解法を用いて解く事ができました。hが十分小さければルンゲ・クッタ法を用いて得た数値解(近似解)は、真の解に十分近いといえるでしょう。今回からは、ルンゲ・クッタ法を常微分方程式を解く信頼できる方法として採用し、具体的な問題を解き得られた解を見るだけでなく、現象に立ち返りその意味を再考してみましょう。(ルンゲ・クッタ法になじめない方はオイラー法を用いるのでも構いません.)

減衰振動再考

前回行った減衰振動についてもう一度考えてみましょう。下の図のように、壁に一端を固定されたバネの他端におもりがつけられている状況を考えます。

このおもりを少しだけ右に引っ張ってから手をはなす。
おもりの中心の時刻tにおけるつり合いの位置からのずれ(変位)を$y(t)$とします。また、時刻0でのおもりの位置を$y(0)=1$、おもりの速度を$y^{\prime}(0)=0$とすると次の単振動の問題となります。
(フックの法則を用いた)

\[
\left\{
\begin{array}{}
y^{ \prime \prime} \ \ &= \ \ -y(t)\\
y(0) \ \ &= \ \ 1 \\
y^{\prime}(0) \ \ &= \ \ 0
\end{array}
\right. \ \ \ \ \ \ (3)
\]

これではおもりに働く力はバネの復元力のみで、ある意味非現実的です。
今回は復元力以外におもりの速度$y^{\prime}$に比例する抵抗が働く場合を考えましょう。(講義ではダンパを加えた場合として説明されています)。
改良されたモデル方程式は次のようになります。

\[
\left\{
\begin{array}{}
y^{ \prime \prime}(t) + 2k y^{ \prime}(t) + \omega^2 y(t) \ \ &= \ \ 0\\
y(0) \ \ &= \ \ 1 \\
y^{\prime}(0) \ \ &= \ \ 0
\end{array}
\right. \ \ \ \ \ \ (4)
\]

ここで$k, \omega$はそれぞれ抵抗力、復元力の強さを表す正の定数です。式(4)の問題を連立1階常微分方程式に変換し、ルンゲ・クッタ法(できなければオイラー法)を用いて解き可視化(グラフ)してください。
(前々回のレポートで提出されているプログラムを使い回してください。それを元に今日は現象を観察してもらいます)


課題3

上記の減衰振動に関して、以下の3通りについて、それぞれグラフを描け。(例えば、$\omega$をある値に固定して、$k$のみを変化させ、下記3通りを満たすような$k$を選ぶとよい。3通りのグラフを色を変えて重ね描きするとよい。)

  1. k<ωの場合:減衰振動
  2. k>ωの場合:過減衰
  3. k=ωの場合:臨界減衰
小ネタ:ドアのダンパ

ところで、ドアは一種のバネ振り子+ダンパ系と見る事ができます。
設計する際、ダンパの強さを臨界減衰になるよう設計するのが一番良いとされます。
なぜでしょうか?考えてみてください。
(良いドアである為には次の二つが重要である。素早くしまる事、しまるときの音が小さい事。)

自動車やバイクのサスペンションも一種のバネ振り子+ダンパ系ですが、おそらくはやはり臨界減衰に近いような設定が良いのであろうと考えられます。(実際にはより複雑な状況もあり、そこまで単純ではないかも知れませんが。)


強制振動

ここからは強制振動系を考えます(参考:計算数理AのHP)。
先の減衰振動系に外から周期的な外力を加える事を考えます。扱う方程式は、以下のようになります。

$$ \frac{d^2y}{dt^2} + 2k \frac{dy}{dt} + \omega^2y \ \ = \ \ A cos \omega_e t\ \ \ \ \ \ (5)$$

見ての通り、右辺が0であれば式(4)の減衰振動系となります。つまりは、式(4)の減衰振動系(放っておけば減衰して静止する)に対して、外から周期的な外力を加えているのが式(5)です。


課題4

式(5)を解くプログラムを作成せよ。下の図のように$y(t)$と$A cos \omega_c t$ を同時に表示せよ。例えばパラメータ、初期値としては、$y(0) =1, y^{\prime}(0)=0, k=0.1, \omega =1, \omega_e =1.3$とし、時間刻み幅hはh=0.1とする。解$y(t)$を赤色、$A cos \omega_e t$を青色で表示することにする。

$A=0$の場合(つまり外力が無い場合)には、

となり、当然ながら減衰振動の様子がみられる。

$A=0.1$の場合には、

となり、過渡応答、定常応答が見られる。

$A=0.1, \omega_e=1.0$の場合には、次のようになる。  


課題5

課題4について、横軸をyとして、質点の動きをアニメーションで示せ。また、外力の項($A cos \omega_e t$)の力の向きと大きさも可視化せよ。

参考用動画($k=0.1, \omega=1.0, \omega_e=1.6, A=0.1$ )

動画中では、青いバーは$cos \omega_e t$を表しており、右側にあるときは右向きに、左側にあるときは左向きに外力がかかっている。(Aの大きさを青いバーの長さに反映させるかどうかは、各自の判断に任せます。)


課題6

(1)$\omega_e=0.2,0.4,0.6,・・・2.0$に対して、$y(t)$と$A cos \omega_e t$の様子(振幅や位相の関係)を見てみよ。

(2)各$\omega_e$に対して、$A=0.1,1.0,10.0$とした場合の$y(t)$と$A cos \omega_e t$の様子(振幅や位相の関係)を見てみよ。

(3)横軸$y$、縦軸$dy/dt$として表示し、振動の様子を見よ。

(1)(2)から分かるように、バネの固有の振動数$\omega$(外力、抵抗が無いときの振動の周期の逆数)と、外力の振動数$\omega_e$が近くなるにつれ、大きな振幅の振動が現れます。
これが「共鳴(共振)」と言われる現象で、例えばブランコを手で押して大きく揺らす事等は(ふつうブランコの揺れに合わせて押すことで振幅を大きくすると思いますが。)、強制振動による共鳴の典型例です。
(逆にこのようなバネ系は、強く揺すってもまあそれ相応にしか振幅はふえません。外からの揺すりの「タイミング」が肝ですね。それを実感してください。)

今回の強制振動については、実は解析的に解(解析解)を求めることができます。課題6が合っているかは、解析解を求め、それと比較してみてください。
(実は解析解がわかってしまえば、下のレポート問題のパラメータはすぐにわかってしまう…)


レポート課題

強制振動の式(5)を用いて、課題5のようなアニメーション
(t=0から100までのアニメーション、時刻$t$もwindow内に表示)を表示するpythonプログラム作成せよ。ただし、以下の仕様を満たすこと。

【仕様1】
$t \geq 70$のときは、
・$t=70$から現在の時刻$t$までの範囲における$y$の最小値、最大値の位置をそれぞれ縦線で図示する。
(つまり$t=80$のときは$70 \leq t \leq 80$の範囲における$y$の最小値、最大値の位置を縦線で図示する。)

【仕様2】
アニメーション終了後、$70 \leq t \leq 100$における$y(t)$の最大値と最小値の差を$D$として、その$D$の値を画面に表示すること。
ただし、$D=1.6$(誤差は3パーセントまで許す)となるように$A, \omega_e$を設定し、$A$の値、$\omega_e$の値はアニメーションのウィンドウ内に表示すること。他のパラメータ($k, \omega$)と初期値は課題4と同じものとする。

【仕様3】
描画する時間tの刻みは、0.1とする.

注意点:
プログラムは関数を用いてアルゴリズムが分かりやすくなるようにしてください。
(注意点:質点の運動が画面内に収まるように座標系を設定して下さい。また$cos \omega_e t$を表すバーの大きさは見やすいように大きさを調整してくれてかまいません。)
ルンゲ・クッタ法、オイラー法のどちらで解いてもかまいませんが、ルンゲ・クッタ法で解いた場合は1割程度高い点をつける予定です。

プログラムを〇〇.py形式(名前は自由)で作成し、Moodleから提出して下さい。
インデント(時下げ)、コメントは必ず行うこと。
また、プログラムの最初の方にコメントとして学生番号と氏名は(最低限)必ず記述すること。
(# bxxxxxx 氏名)
提出期限2023年5月19日(金)23時59分


課題7

バネは大きく伸び縮みすると急激に硬くなるような場合もあります.そのようなバネに対する強制振動系として、次の微分方程式

\[
\left\{
\begin{array}{l}
\frac{dy}{dt} \ \ &= \ \ v\\
\frac{dv}{dt} \ \ &= \ \ -0.3v – y^3 + B cos(t)
\end{array}
\right. \ \ \ \ \ \ (6)
\]

を考えます。この式を数値的に解いて、$B=0,1,2, \cdots, 12, \cdots, 60, \cdots$等いろいろな場合の$y$の時間変化を調べてください。特に$y,v$の初期値が少し違う場合の、$y$の時間変化を比較してください。課題2、3とどのような特徴の違いがあるでしょうか.(十分大きい$t$までの結果を見てみてください。また横軸$y$、縦軸$v$として表示した場合の様子も見てみてください.)

ハードディスクから原子炉まで、様々な機械の「振動」が、このような方程式を応用しながら解析されています。


自然界の振動

上記では、摩擦が働く等して放っておいたら止まってしまうようなシステム(バネ系)が、外部からの「振動(時間に依存して振動的に変化する外力)」を受ける事で定常的に振動する様子について考察しました。
しかし実際には、外からの影響が「振動的」でなくても(外力が時間変化しない、つまり方程式の右辺に「$t$」が現れない)、システム自身が振動的になる場合があります。そのような振動は「自励振動」と呼ばれます。
以下、自励振動の簡単な例題を見ていきます。生物を含む自然界の様々な場面で見られるいろいろな「振動(リズム)」の多くが、自励振動であります。(体内時計、脳波、生態系、為替変動、蛇口の水滴、地震、気象、..等等.宇宙や惑星の運動等は少々違います…)


課題8

次の連立微分方程式を適当な初期条件のもと、オイラー法かルンゲ・クッタ法で解け。

\[
\left\{
\begin{array}{l}
\frac{dx}{dt} \ \ &= \ \ 1-(1+b)x+x^2y\\
\frac{dy}{dt} \ \ &= \ \ bx – x^2 y
\end{array}
\right. \ \ \ \ \ \ (7)
\]

※ bの値が2より大きいか小さいかで振る舞いが変わります。

この方程式は、ブラッセレーター方程式といわれるもので、BZ反応と呼ばれる、時間的に化学成分の濃度が振動するような化学反応の振動をモデル化した方程式です。(BZ 反応とは何でしょう?その存在は古くから知られているものの、最近でも当時の高校生たちが新しい発見をしています。調べてみましょう。)


課題9

次の連立微分方程式を適当な初期条件のもと、オイラー法かルンゲ・クッタ法で解いてください。

\[
\left\{
\begin{array}{l}
\frac{dx}{dt} \ \ &= \ \ 3x-xy\\
\frac{dy}{dt} \ \ &= \ \ -y+ xy
\end{array}
\right. \ \ \ \ \ \ (8)
\]

課題8の方程式同様、この方程式も振動解を見せますが、その特徴は少々違っております。
例えば初期条件を変えた場合に、これらの(課題8と課題9それぞれの)方程式系の解の振る舞いが、どのように変化していくか、見比べてください。

この方程式は、ロトカ・ボルテラ方程式といわれるもので、生態系内のある被食者と補食者の数 (x, y) 増減関係を簡単にモデル化した方程式です。
(各時刻、被食者はある割合(3x)で増えて、補食者に出会うと食べられ減る(-xy)。同様に捕食者は被食者 に出会うとそれを食べて増えて(xy)、ある割合で(-y)で寿命等で減っていく。)詳しい事は各自調べてください.


課題10

惑星の運動を考える。万有引力の法則から2物体(質量はそれぞれ$M$ 、$m$)に働く力は$\frac{GMm}{r^2}$となる。$r$は2物体間の距離、$G$は万有引力定数で$G \approx 6.674 \times 10^{-11} \ \ [m^3/(kg/s^2)]$である。原点に太陽(質量 $M \approx 1.989 \times 10^{30} [kg]$ )があり、惑星(質量 $m$)の位置を

\[
\vec{r}(t)=
\begin{pmatrix}
r_1(t)\\
r_2(t)\\
r_3(t)
\end{pmatrix}
\]

とする。惑星に働く力は

$$ \vec{F} \ \ = \ \ – \frac{GMm}{| \vec{r} (t)|^2}\cdot \frac{ \vec{r} (t)}{| \vec{r} (t)|} \ \ = \ \ -GMm \frac{ \vec{r} (t)}{| \vec{r} (t)|^3}$$

となり、

$$ \vec{F} \ \ = \ \ m \vec{a} \ \ = \ \ m \vec{r}^{\prime \prime} $$

より$g=GM$とおくと、2階の常微分方程式の初期値問題

\[
\left\{
\begin{array}{}
\vec{r}^{\prime \prime}(t) \ \ &= \ \ -g \frac{\vec{r}(t)}{|\vec{r}(t)|^3}\\
\vec{r}(0) \ \ &= \ \ \vec{r}_0\\
\vec{r}^{\prime}(0) \ \ &= \ \ \vec{v}_0
\end{array}
\right. \ \ \ \ \ \ (9)
\]
が得られる。これを1階の常微分方程式系に直し、ルンゲ・クッタ法を用いて数値的に解き、惑星が太陽に衝突せずに周回軌道を取れるような初期条件を求めよ。

メモ:

実際には$GM \approx 1.3725 \times 10^{20} [m^3 / s^2]$ ではありますが、簡単のために$GM=1$、初期時刻での$|vec{r}(0)|$は1から10程度の値を用いて良いです。実際の値を使うのであれば、地球の太陽からの距離が$1.4960 \times 10^8 \ \ [km]$、公転周期が約365日であることを考えて、

\[
\left.\
\begin{array}{l}
GM &\approx 9.9097 \times 10^{-4} [(10^8km)^3 / Day^2]\\
&\approx 1.3202 \times 10^2 [m^3 / Year^2]
\end{array}
\right. \ \ \ \ \ \ (10)
\]
あたりを使って計算すると良いでしょう。

難しいようであれば、まずは $r_3$ を考えずに、$r_1-r_2$ 平面上の運動方程式として考えていくのも良いでしょう。

また、3次元のプロットは教えていません。$r_1-r_2$平面・$r_2-r_3$平面・$r_1-r_3$平面を並べるなどで断面図を描きましょう。どうしても3次元のプロットをしたい場合は、各自で調べてみてください。


課題11

水星は太陽に非常に近く、水星の効果が他の惑星の公転に影響する。そのため、惑星の運動は以下のような方程式に従うようになる。

$$ \frac{d^2 \vec{r}(t)}{dt^2} \ \ = \ \ -g \frac{\vec{r}(t)}{|\vec{r}(t)|^3} + h \frac{\vec{r}(t)}{|\vec{r}(t)|^4}$$

課題5と同様に数値的に解をもとめよ。ただし、$h<<g$とする。


付録:ルンゲ・クッタ法

1階常微分方程式のルンゲ・クッタ法

オイラー法は単純でそれはそれでよいのですが、実際の解に近い数値解を出すためには、時間刻み幅$h$をかなり小さくとらねばならないことが課題の結果からわかりました。一方、時間刻み幅を小さくしすぎると、計算コストがかかる(計算を大量にする必要になる)ことが問題です。そこで、より精度の高い解法が必要となります。そのような問題点の解法として、ルンゲ・クッタ法を扱い、それを用いた数値解法の演習を行います。

この演習ではせいぜい3、4変数の連立微分方程式を解くので、計算コストについてはそこまで問題になりませんが、例えば、生体分子のシミュレーションでは1,000や10,000のオーダーの変数を扱います。そうすると計算コストをきちんと考えてシミュレーションしないと、シミュレーション終了まで月・年単位でかかってしまうこともあります。そのようなことを防ぐため、様々な解法やクラスターマシンやスパコンといった大規模な計算システムを有効に活用して計算を行います。

ルンゲ・クッタ法の導出については、このページの最後を参照してください(詳細は参考書を参照してください)。
ルンゲ・クッタ法は次に示すアルゴリズムで、オイラー法に比べれば多少複雑なものとなっています。
比較の為に両方のアルゴリズムを載せておきます(赤字の部分が異なります)。
よく比較してください。

オイラー法ルンゲ・クッタ法
T :解を求める時間の上限
h :時間刻み幅
N :時間刻み数
y:変数
a:初期値
y_new:実数型の変数
T :解を求める時間の上限
h :時間刻み幅
N :時間刻み数
y:変数
a:初期値
y_new, r_1, r_2, r_3, r_4 :実数型の変数
T, N, h を定める
a の入力を受ける(input関数とfloat)
y = a

i = 0, 1, 2, … , Nの順に
t = i*h

y_new = y + h*f(t, y) 
y = y_new
一定間隔毎にグラフを描画(ax.set_data関数とplt.pause関数)を繰り返す
f(t, y) は関数として定義するとよい.
T, N, h を定める
a の入力を受ける (input関数とfloat)
y = a

i = 0, 1, 2, … , Nの順に
t = i*h

r_1 = f(t, y)
r_2 = f(t + h/2, y + (h/2)*r_1)
r_3 = f(t + h/2, y + (h/2)*r_2)
r_4 = f(t + h, y + h*r_3)
y_new = y + (h/6)*(r_1 + 2*r_2 + 2*r_3 + r_4)

y = y_new
一定間隔毎にグラフを描画(ax.set_data関数とplt.pause関数)を繰り返す
f(t, y) は関数として定義するとよい.

* オイラー法では, (とびとびの)時刻 t = jh での値 y から 次の(とびとびの)時刻 t = (j+1)h での値 y_new を直接計算しています。
本来、微分方程式では時刻は連続ですが、この場合は時刻 t = jh ~ (j+1)h の間の事は一切考慮されておらず、その分誤差が現れてきます。

* ルンゲ・クッタ法では, この t = jh ~ (j+1)h の間の情報を (r_1, ~ r_4 を使って ) いくらか取り入れる事で、よりよい近似計算が可能になっています。
勿論誤差は現れるのですが、オイラー法と比べて雲泥の差です。
r_1〜r_4を求めることで処理は増えますが、それと比較しても同じ計算精度を実現するときの計算回数は非常に少なくなります(上の課題1、2あたりで同じ計算精度を実比較するときの時間刻み数Nを比較してみてください)。

2階(n階)常微分方程式のルンゲ・クッタ法

前回の課題で扱った

\[
\left\{
\begin{array}{l}
y^{\prime \prime}(t) \ \ &= \ \ – \omega^2 y(t)\\
y(0) \ \ &= \ \ 1\\
y^{\prime}(0) \ \ &= \ \ 0
\end{array}
\right. \ \ \ \ \ \ (11)
\]
を$y_1(t)=y(t), y_2(t)=y^{\prime}(t)$とおいて、次のように連立の1階の常微分方程式に帰着したもの

\[
\left\{
\begin{array}{l}
y_1^{\prime}(t) \ \ &= \ \ y_2(t)\\
y_2^{\prime}(t) \ \ &= \ \ – \omega^2 y_1(t)\\
y_1(0) \ \ &= \ \ 1\\
y_2(0) \ \ &= \ \ 0
\end{array}
\right. \ \ \ \ \ \ (12)
\]

を再び考えます。
このような連立の常微分方程式をルンゲ・クッタ法を用いて解く場合のアルゴリズムを次に示します。

2階常微分方程式のルンゲ・クッタ法
T :解を求める時間の上限
h :時間刻み幅
N :時間刻み数
Y1, Y2:変数
a1, a2:初期値
Y1_new, Y2_new, r1_1, r1_2, r1_3, r1_4, r2_1, r2_2, r2_3, r2_4 :実数型の変数
T, N, h を定める
a1, a2 の入力を受ける(input関数とfloat)
y1 = a1
y2 = a2

i = 0, 1, 2, …. ,Nの順にt = i*h
r1_1 = f1(t, y1, y2)
r2_1 = f2(t, y1, y2)
r1_2 = f1(t + h/2, y1 + (h/2)*r1_1, y2 + (h/2)*r2_1)
r2_2 = f2(t + h/2, y1 + (h/2)*r1_1, y2 + (h/2)*r2_1)
r1_3 = f1(t + h/2, y1 + (h/2)*r1_2, y2 + (h/2)*r2_2)
r2_3 = f2(t + h/2, y1 + (h/2)*r1_2, y2 + (h/2)*r2_2)
r1_4 = f1(t + h, y1 + h*r1_3, y2 + h*r2_3)
r2_4 = f2(t + h, y1 + h*r1_3, y2 + h*r2_3)
y1_new = y1 + (h/6)*(r1_1 + 2*r1_2 + 2*r1_3 + r1_4)
y2_new = y2 + (h/6)*(r2_1 + 2*r2_2 + 2*r2_3 + r2_4)
y1 = y1_new
y2 = y2_new
一定間隔毎にグラフを描画 (lines.set_data関数とplt.pause関数)を繰り返す
f1(t, y1, y2), f2(t, y1, y2) は関数として定義するとよい.

また、f1(t, y1, y2) および f2(t, y1, y2) はそれぞれ実数型の関数です。
11)式の場合には f1 が第一式の右辺, f2 が第二式の右辺となります。

(今回, ω = 1 とします.)

小ネタ:変数名の付け方

言語によっては、意外な変数名がすでに使用されていることがあります。例えばC言語では、y0, y1, ynはグローバル変数の名前として好ましくありません(調べてみましょう)。y_0, y_1, y_nや、Y0, Y1, Ynで代用しましょう。また、すでに気づいている人もいるかもしれませんが、pythonでは「lambda」は無名関数で使うため、使えません。「Lambda」などで代用しましょう。

ルンゲ・クッタ法の仕組み(2段公式を例に)

簡単にルンゲ・クッタ法について説明します。演習で取り扱ったルンゲ・クッタ法は4段公式とよばれるものですが、ここでは簡単の為、2段公式を取り扱います。

次の常微分方程式の初期値問題の数値解を求めたいとしましょう。

\[
\left\{
\begin{array}{l}
\frac{dx}{dt} \ \ &= \ \ f(t,x)\\
x(t_0) \ \ &= \ \ x_0
\end{array}
\right. \ \ \ \ \ \ (R1)
\]
2段公式とは、$t=t_n$で(R1)の近似解$x_n$が求められたとして、時間ステップ幅$h$だけ進んだ$t_{n+1}=t_n +h$における値$x_{n+1}$を次の形で計算する公式です。

\[
\left\{
\begin{array}{l}
x_{n+1} \ \ &= \ \ x_n + ( \gamma_1 k_1 + \gamma_2 k_2)\\
k_1 \ \ &= \ \ hf( t_n, x_n)\\
k_2 \ \ &= \ \ hf( t_n + \alpha h, x_n + \beta k_1)
\end{array}
\right. \ \ \ \ \ \ (R2)
\]
つまり、$x_n$から$x_{n+1}$へ進む1ステップにおいて方程式(R1)の第一式の右辺の$f(t, x)$を2回計算するので、2段公式と呼びます。この2段公式には、パラメータ$ \alpha, \beta, \gamma_1, \gamma_2$が含まれますが、これらは方程式(R1)の真の解$x(t)$のテイラー展開

$$x(t_n+h) \ \ = \ \ x(t_n)+hf(t_n, x(t_n))+ \frac{1}{2!} h^2 \{ f_t(t_n, x( t_n)) + f_x(t_n, x( t_n))f(t_n, x( t_n))\} + O(h^3) \ \ \ \ \ \ (R3)$$

と(R2)式の第一式の同様の展開とが$h$のなるべく高い次数の項まで一致するように定めます。
つまり、$x_n=x(t_n)$と仮定して(R2)式の$k_2$の式の右辺を$f(t,x)$周りでテイラー展開すると、

$$x_{n+1} \ \ = \ \ x_n+h(\gamma_1 + \gamma_2) f(t_n, x_n) + h^2 \gamma_2\{ \alpha f_t(t_n, x_n) + \beta f_x(t_n, x_n) f(t_n, x_n)\} + O(h^3) \ \ \ \ \ \ (R4)$$

となります。
これと(R3)が$h^2$の項まで一致するように

\[
\left.
\begin{array}{l}
\gamma_1+\gamma_2 \ \ &= \ \ 1\\
\gamma_2 \alpha = \gamma_2 \beta \ \ &= \ \ \frac{1}{2}\\
\end{array}
\right. \ \ \ \ \ \ (R5)
\]

とおく。公式(R2)で定められる近似解のテイラー展開が$h$の2次の項まで真の解のテイラー展開に一致するという意味で、この公式を2次の公式と呼びます。
この公式は2段公式でもあるので、2段2次の公式とも呼ば れます。パラメータを定める方程式(R5)には自由度が2残っているのでその定め方によって色々な公式が導かれます。(改良オイラー法、ホイン法、修正オイラー法など)
演習で用いた4段4次のルンゲ・クッタ法の公式も同様にして求めることが出来ます。

計算数理A演習第9回

コンテンツ

第9回

フーリエ級数

周期関数u(t)が「(第一種不連続点のみをもつ)区分的に連続な周期2Tをもつ周期関数」であるとき、フーリエ級数は元の関数u(t)に各点収束します。
よって、u(t)2T周期の関数とすると、

\displaystyle u(t) &=& \frac{a_0}{2} + \sum_{n=1}^\infty \left(a_n \cos \frac{\pi nt}{T} + b_n \sin \frac{\pi nt}{T} \right),\vspace{0.5}
\displaystyle a_n &=& \frac{1}{T}\int_{-T}^{T} u(t) \cos \frac{\pi nt}{T} dt,\vspace{0.5}
\displaystyle b_n &=&\frac{1}{T}\int_{-T}^{T} u(t) \sin \frac{\pi nt}{T} dt.

が成り立ちます。特にa_nb_nの先頭の 1/T の部分は 2/(積分区間) と考えると間違いが少ないです。
詳細は計算数理Aの講義で説明があると思いますので、ざっくりと概要だけ述べると、どんな周期関数も三角関数の線型結合で表現できて、各三角関数の重みがa_nb_nである、ということです。

無限級数をとったとき、フーリエ級数が元の関数に収束することは数学的に証明されています。
では、無限に足し合わせるのではなくて、有限個だけ足し合わせた場合は、どのような曲線が得られるでしょうか?
今回の演習では、足し合わせる項の数を増やして行ったときに、どのようにもとの関数u(t)に収束して行くのかをシミュレーションを通して見ていきましょう。

また、u(t)が不連続面を含む場合において、「滑らかな三角関数だけを足し合わせて、不連続な関数を近似する」という一見無茶なことをどのように実現しているのかを確認してみてください(課題3、4)。


課題1(ジグザグ波)

u(t) = |t| on [-1, 1]

を周期2T~(T=1)の関数として拡張したものを考える。 この関数のフーリエ級数展開は、

\[ u(t) = \frac{1}{2} -\frac{4}{\pi^2}\sum_{n:odd} \frac{1}{n^2} \cos \pi n t \]

となる。ただし、n:oddとはnが奇数であること(つまりn=1, 3, 5, \cdotsについて和をとること)を表している。 ここで、足し合わせるnの最大値をNとして、

\[ u_N(t) = \frac{1}{2} -\frac{4}{\pi^2}\sum_{n:odd}^N \frac{1}{n^2} \cos \pi nt \]

とおき、N=1, 3, 5, \cdotsの場合における、u_N(t)のグラフを図示せよ。
その際、u(t)のグラフも重ねて描画すること。 ただし、描画するtの範囲は-2から2とせよ。 (Nが大きいときについて:\cos \pi Ntがそこそこまともに描画できる程度に、tの刻み幅を小さくすることを意識してください)


小ネタ:関数の引数の参照渡しと値渡し

(少し難易度高めです。詳しいことは述べません。興味がある人は調べてみてください。)
関数にリスト(配列)を渡してまとめてできたらなぁ、と思う人がいるかもしれません。思わないし、別に興味ない、と言う人はこの小ネタは飛ばしてください。Pythonでは、関数の引数にリストを指定することも可能です。例えば、課題1において、以下の関数を定義すると便利かもしれません。

def a_n(n): 
    return -4/(np.pi*np.pi*n*n)

こうすれば、関数a_nにnが1, 3, 5, … , Nというリストの形で入ってきても、a_n(1), a_n(3), …, a_n(N)というリストの形で返ってきます(※ 「リスト * リスト」はリストの要素ごとの掛け算。内積ではない)。

では、u(t)についても同様に関数を定義しようと思います。

import numpy as np
import matplotlib.pyplot as plt
T=1
M=100

def u(tau): 
    for i in range(len(tau)): 
        while tau[i] > T: 
            tau[i] = tau[i] - 2*T 
        while tau[i] < -T: 
            tau[i] = tau[i] + 2*T 
    return np.abs(tau)

t = np.linspace(-2,2,M+1)
u_ans = u(t)

fig = plt.figure(figsize=(14,8))
ax = fig.add_subplot(111)

ax.plot(t, u_ans, "r-",label=r"ZigZag") 

こうすれば、周期関数もうまく値が入ってくれそうです。ではこの状態で横軸をリストt、縦軸をu_ansでプロットすると、tがおかしなことになっていることがわかるとおもいます(下図)。

これは、Pythonでは、引数の受け渡しの実態は、値そのものではなく、値が格納されている場所(変数が参照している場所)を渡しているからです。これを参照渡しと呼びます。(対して、値そのものを受け渡していることを値渡しと呼びます。)そのため、関数の中で変数tau(仮引数)を変更すると、同時に関数の外の変数t(実引数)も変更していることになってしまっており、グラフ描画の際にtが元々の値から変わってしまったわけです。

ちなみに、Pythonでは引数を参照渡しで渡していますが、実変数を変更される型(mutable型、リスト・辞書型など)と変更されない型(immutable型、数値(スカラー)・文字列など)があります。immutable型の場合は、きちんと代わりの変数の場所が用意され、実引数は影響を受けません(そのため見かけ上は値渡しと同じように動作します)。

では、実際にリストを引数とするときに、実引数に影響を与えないようにするにはどうすれば良いのか、ですが、以下のようにcopyモジュールのdeepcopyを使います。

import numpy as np
import matplotlib.pyplot as plt
import copy
T=1
M=100
def u(tau): 
    tmp = copy.deepcopy(tau) 
    for i in range(len(tmp)): 
        while tmp[i] > T: 
            tmp[i] = tmp[i] - 2*T 
        while tmp[i] < -T: 
            tmp[i] = tmp[i] + 2*T 
    return np.abs(tmp)

t = np.linspace(-2,2,M+1)
u_ans = u(t)

fig = plt.figure(figsize=(14,8))
ax = fig.add_subplot(111)
ax.plot(t, u_ans, "r-",label=r"ZigZag") 

こうすることで、実引数の値そのものを受け渡すことができます。単にtmp = tauとしてしまうと、また参照渡しになってしまうので、無意味になってしまいますのでご注意を。

この辺りの話は、C言語でも配列を引数にする方法があるにはありますが、そのために「ポインタとアドレス」という概念を学ぶ必要があります(計算数学演習では扱いません)。このような概念を知っていると、他の言語への応用がきき、両方の言語のより深い理解に繋がります。


課題1′

課題1 では、すでに数値積分を解析的に解いた式をプロットしたが、すでに諸君は数値積分で近似的に求める方法を知っているはずである。そこで、関数u(t)さえ定義してしまえば、自動的にa_nおよびb_nが求まるような仕様に変更せよ。さらに、横軸をnとし、縦軸にa_nおよびb_nとしたグラフも表示するプログラムを作成せよ。(課題1の場合、b_nnによらずほぼ0になるはずである。)


課題2(ジグザグ波の続き)

\[ d(t, N) = |u_N(t) - u(t) | \]

とする。このとき、-1 \le t \le 1におけるd(t, N)の最大値をD(N)とおき、
D(N)を画面に表示できるように、課題1のプログラムを改良せよ。
Nを動かして、D(N)0に収束するかどうか検証せよ。

dは元の関数とu_N(t)とののズレを表している。Dはもっともずれの大きい場所のdの値に相当する。)


課題3 ギブス現象

次の式(矩形波)

\[ u(t) = \left\{ \begin{array}{cc} 1 & (0 < t < \pi) \\ -1 & (-\pi < t < 0) \end{array} \right. \]

を周期2\piの関数として拡張したものを考え、不連続面(t=0)に注目する。

u(t)を有限のNに対してフーリエ級数展開した関数をu_N(t)とし、Nの値を変化させた場合において、不連続面近くのグラフを描画せよ。

ただし、描画する範囲を、-0.5<t<0.5として、
キーボードからNの入力を促し、u_N(t)u(t)を重ねて描画せよ。

参考:N=21のときのグラフ


課題4

課題3の式について、不連続点以外のtについて、u_N(t)u(t)に収束するかどうかを確認するため、

\[ d(t, N) = |u_N(t) - u(t)| \]

とし、d(0.4, N)N=1, \cdots ,100でどのように変化するかグラフで示せ。

計算数理A演習第10回

コンテンツ

第10回

1次元拡散方程式の数値解法

これまでは変数が時間にのみ依存するものを扱っていましたが、今回からは、時間以外に空間にも依存する変数を扱います。つまり、変数としてはu(x,t)と書けます。1次元拡散方程式(1次元熱伝導方程式とも呼ばれる)の初期値境界値問題を数値的に解きます。
それは、次のようなものです。(教科書や文献によって記号など多少異なるかもしれません。)

問題1.1

\displaystyle \frac{\partial u(x, t)}{\partial t} = D\frac{\partial^2u(x, t)}{\partial x^2} ~~ (0 < x < L, t> 0),
u(0, t) = 0,
u(L, t) = 0~~(t>0),
u(x, 0) = 2x(L-x)~~(0\le x \le L)

1行目の式は拡散方程式、もしくは熱伝導方程式と呼ばれます。式中のDは拡散係数または熱伝導率とよばれます。
拡散とは物が散らばっていく様子(例:コップにインクを垂らすと、時間とともに広がって最後には一様になる)であり、熱伝導とは熱が伝わる様子(例:スプーンを熱湯に浸けていると徐々に熱が伝わって持っている部分まで熱くなる)です。
これらの現象を表現しているのが上式なのです。
「物の散らばり」と「熱の伝わり」という異なる現象が同じ方程式で表されるというところは、数式の面白さといって良いでしょう。
熱伝導方程式と見るとu(x,t)は時刻tでの温度分布、拡散方程式とするとu(x,t)は粒子の密度分布に対応し、何れにしても、分布が時間的な変化を考えることができます。

2行目と3行目の式は境界条件と呼ばれ、空間の端の部分(x=0およびx=L)での拘束条件になります。
問題1では、x = 0x = Lでいつもu = 0となるディリクレ境界条件を境界に課しています。
温度の分布の言葉でいうと、両端を氷か何かでつねに冷やしている状況を考えていることになります。
つまり、熱が両端からどんどんと奪われているわけです。

4行目の式は、初期条件(t=0)です。

以下では、拡散方程式・熱伝導方程式という呼称を拡散方程式に統一します。


空間と時間の分割

さて、計算機で扱えるよう離散化し、数値的に近似解を求めます。
これまでの常微分方程式の解法では、数値計算において、時間という本来連続な変数をそのまま扱うわけにはいかないため、t=t_i~(i=0,1,\cdots,N)のように離散化をしていたわけですが、もう一つ増えた空間を表す変数xについても同様に離散化をする必要があります。ただし、以下のように、空間方向の分割方法は時間方向の分割方法とは少し異なります。

上図の(1)は時間方向の分割方法で、これまでと変わりません。数直線で書き表すと、考える範囲をI等分した目盛り上に位置します。一方、空間方向の分割は、考える範囲をI等分するところまでは同じようにしますが、そこから目盛と目盛の中間の点に空間座標がきます。なぜこのような面倒なことをするのか、というと、境界条件について考えやすいためです(後述)。

空間の区切り方のイメージとしては、アパートのように同じ間取りの部屋で区切り、隣との壁が目盛り、物質の濃度や温度は部屋の中心で測る、といった感じでしょうか。アパートでは隣の部屋の物質が壁を通過して流れ込んだりすることはそうそうないですが、断熱材が少ないアパートは熱は多少なりとも伝わっていったりします。両隣の部屋がエアコンをガンガンにつけていると、真ん中の部屋の光熱費は少なくて済むかも?

これまでhを時間刻み幅としてきましたが、今後はhを空間刻み幅、\tauを時間刻み幅とします。すなわち、空間方向では[0, L]区間をN等分(h=L/N)、時間方向では[0, T]区間をM等分(\tau=T/M)して、x_j = (j - 1/2)h, t_k = k\tauおくことができ、

\[ U^k_j \approx u(x_j, t_k), \ \ (j=1, \cdots, N, k= 0, 1, \cdots , M)\]

と書くことができます。

差分方程式

上記の拡散方程式と境界条件・初期条件を以下のように差分化します。

拡散方程式の差分化

拡散方程式は、上記の差分化を使えば、

$$\frac{\partial u(x, t)}{\partial t} = D\frac{\partial^2u(x, t)}{\partial x^2} $$

\[ \frac{\partial u(x, t)}{\partial t} = D\frac{\partial^2u(x, t)}{\partial x^2} \]

は次の差分方程式で近似されます。

$$ \frac{1}{\tau} (U_j^{k+1} – U_j^k) = D\frac{\frac{U_{j+1}^k – U_j^k}{h} – \frac{U_j^k – U_{j-1}^k}{h}}{h}, \ \ (j=1, \cdots, N, k= 0, 1, \cdots ,M)$$

これを整理すると、

$$ \frac{1}{\tau} (U_j^{k+1} – U_j^k) = \frac{D}{h^2}(U_{j-1}^k – 2U_j^k + U_{j+1}^k), \ \ (j=1, \cdots, N, k= 0, 1, \cdots ,M)$$

になります.さらに,これを書き換えれば,

$$U_j^{k+1} = \alpha U_{j-1}^k + (1 – 2\alpha) U_j^k + \alpha U_{j+1}^k, \ \ (j=1, \cdots, N, k= 0, 1, \cdots ,M)$$

が得られます。ただし、\alpha = D\tau / h^2としました。
この式は見てのとおり、時刻t_kでのuから次の時刻t_{k+1}でのuの値が計算できる形をしています。

境界条件の差分化

今回は境界条件はディリクレ境界条件ですが、他にもよく使う境界条件であるノイマン境界条件・周期境界条件も導出しておきましょう。

ディリクレ境界条件

ディリクレ境界条件の一般的な形式は次の通りです。

\[u(0, t) = c,u(L, t) = c~(t>0)\]

x=0について考えると、x=0の目盛り上で時間に依らず常にある一定値c(今回はc=0)となるためには、境界の両隣の2点の中間値がcになれば良いので、

\[\displaystyle \frac{U_0^k+U_1^k}{2} = c\]

より、

\[U_0^k=2c-U_1^k~ (k=0, 1, 2, \cdots ,M)\]

とすれば良いです。反対側のx=Lの境界も同様に

\[U_{N+1}^k = 2c-U_N^k~ (k=0, 1, 2, \cdots ,M) \]

となります。

ノイマン境界条件

ノイマン境界条件は以下の通りです。

\[\frac{\partial u(0, t)}{\partial x} = 0, \frac{\partial u(L, t)}{\partial x} = 0~~(t>0)\]

x=0について考えると、x=0の目盛り上で時間に依らず常に空間方向の微分が0とならなければならないのですが、空間方向は離散的に区切られています。そこで、

\[\frac{\partial u(0, t)}{\partial x}\approx\frac{U_1^k-U_0^k}{h} = 0\]

より、

\[U_0^k=U_1^k~ (k=0, 1, 2, \cdots ,M)\]

とすれば良いです。反対側のx=Lの境界も同様に

\[U_{N+1}^k = U_N^k~ (k=0, 1, 2, \cdots ,M) \]

となります。

周期境界条件

周期境界条件は、周期的になっていれば良いのですが、周期関数の定義

\[u(x)=u(x+L)\]

を使って考えます。境界を超えて、x<0x>L” height=”12″ width=”46″>も同じように関数が繰り返されると考えれば良いので、</p>



<figure class=U_0^k=U_N^k

U_{N+1}^k=U_1^k

となります。

初期条件

初期条件は、xの離散化のみ考えればよく、

$$U_j^0 = 2x_j(L-x_j) ~(j=1, 2, \cdots ,N)$$

になります。

これで、準備はできました。

次に拡散方程式を計算機にて解くためのアルゴリズムを考えましょう。

計算ルーティンについては以下のイラストを参照してください.


解を求める時刻の上限をTとし、適当な自然数Mを定めて、\tau = T/Mとします。
問題1.1を解くアルゴリズムは次のようになります(ただし、D=1.0とした)。

アルゴリズム

ライブラリのインポート
(1) 変数と初期パラメータを設定
T(時間上限), M(時間刻み数), L(空間幅), N(空間刻み数), D(拡散係数) を設定する
h = L/N, τ = T/M, α = Dτ/(h^2) (プログラム中では, τは「tau」, αは「alpha」等とする.)
x[N+2], u[N+2], new_u[N+2](偏微分方程式の変数をリストとして準備する。)
 
(2) 初期値設定
j := 1,....,N の順に 
    x[j] = (j - 0.5)*h 
    u[j] = 2*x[j]*(1-x[j])
を繰り返す

u[0] = -u[1], u[N+1] = -u[N]   (境界条件)

描画する準備
figとaxをつくり、最初の状態のuを描画する。

(3)時間発展を計算
k := 0,1,.....,M-1 の順に 
    (3.1)計算 
    j := 1,2,....,N の順に 
        new_u[j] = αu[j-1] + (1 - 2α)u[j] + αu[j+1] 
    を繰り返す 
    u[1:-1] = new_u[1:-1] (リストの更新) 
    u[0] = -u[1], u[N+1] = -u[N]   (境界条件) 
    (3.2)(x[1], u[1]) ~ (x[N], u[N]) を画面に表示 
    (ただし x[0], u[0], x[N+1], u[N+1] は境界処理用なので表示しない) 
    更新されたuを描画 
    plt.pause関数で絵を更新
を繰り返す

アルゴリズム(2時次元ベクトル配列を用いる場合)

ライブラリのインポート
(1) 変数と初期パラメータを設定
T(時間上限), M(時間刻み数), L(空間幅), N(空間刻み数), D(拡散係数) を設定する
h = L/N, τ = T/M, α = Dτ/(h^2) (プログラム中では, τは「tau」, αは「alpha」等とする.)

xとtの一次元ベクトルを作成する(np.linspace推奨)

Uの二次元ベクトル用配列を作成する(0詰めする.行は"時間に必要な数",列は"空間に必要な数"にしておくと楽)
ちなみに
U=np.zeros((3, 4))
と書くと3行,4列の0詰めした2次元ベクトル用配列が作成される.
 
(2) 初期値設定
#初期条件
U[0]=初期条件式   (U[0]=と指定すると"Uの0行目"に一気に値が代入される.列に代入したい人はU[:,0])

#(境界条件)
U[0][0] = -U[0][1], U[0][N+1] = -U[0][N]   

(3)時間発展を計算
k := 0,1,.....,M-1 の順に 
    j := 1,2,....,N の順に 
        U[k+1][j] = αU[k][j-1] + (1 - 2α)U[k][j] + αU[k][j+1] (更新式) 
    #(境界条件)
    ここに書く


### 描画する準備 #####
figとaxをつくり、最初の状態のuを描画する。
後は第7回講義の方法でanimation化

このアルゴリズムを実現するプログラムを作成せよというのが当面の課題となります。


課題1

上のアルゴリズムに従ってプログラムを完成させよ。
ただし、T=0.5, M=5000, L=1.0, N=50, D=1.0程度、
初期条件はu(x,0) = 2x(1-x)、境界条件はディリクレ条件とする。


実際の計算部分を正しくプログラムにすることができれば、次のようなアニメーションを見ることができます。
両端から熱(物質)が奪われ、全体の温度(密度)が下がることがわかります。

計算数理A演習第11回

コンテンツ

第11回

1次元拡散方程式の数値解法(その2)

次の問題を考えます。先週の問題1.1と少し違いますので注意してください(初期値と x の範囲が異なる)。

問題1.2 

\[
\left\{
\begin{array}{l}
\frac{\partial u(x, t)}{\partial t} \ \ &= \ \ D \frac{\partial ^2 u(x, t)}{\partial x^2} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (x \in (0, \pi), t >0)\\
u(0, t) \ \ &= 0 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (t>0)\\
u( \pi, t) \ \ &= 0 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (t>0)\\
u(x, 0) \ \ &= \ \ sin(x) + sin(7x) \ \ \ \ \ \ (x \in [0, \pi])\\
\end{array}
\right. \ \ \ \ \ \ \ \ (1)
\]  


課題1

問題1.2の解を数値的に求めるプログラムを作成せよ。


問題1.2の数値計算結果例

※ 上のアニメーションではD=0.04としている。


課題2

(初期値のsinの部分をcosに書き換えて)境界条件をノイマン条件に変更すると、どのような振る舞いをするか調べよ。
ノイマン条件にするには、式(4)の代わりに

\[ U_0^k = U_1^k, U_{N+1}^k = U_N^k \ \ (k=0, 1, 2, \cdots) \]

とすればよい。


先に出てきた、

\[ U_j^{k+1} = \alpha U_{j-1}^k + (1-2\alpha)U_j^k + \alpha U_{j+1}^k \]

を繰り返し計算し、近似解を求める方法を陽解法と呼びます。
(前の時刻t_nでの値から次の時刻t_{n+1}の値を直ちに計算できる形の差分方程式を陽公式(explicit scheme)と呼びます。陽公式を使った解法なので陽解法と呼びます。)
天下り的になりますが、拡散方程式の数値解法として陽公式を用いる場合、\tauhDは次の関係を満たす必要があります(安定性条件)

(S) \begin{equation*} \alpha = D\tau/h^2 < 1/2 \end{equation*}

ここでは、この安定性条件を破った場合に計算がどのようになるかを見ます。

安定性条件の導出は、フーリエ級数展開を用いたノイマンの安定性解析法を用います。ノイマン法を用いると、各周波数の振幅に関する常微分方程式が現れます。これを解いて、振幅が収束するか、発散するか議論すれば良いわけです。詳しくは講義で紹介されます。


課題3

課題1のプログラムにおいてのパラメータ N (空間刻み数)やパラメータ M (時間刻み数) 等を調節し、安定性条件(S)をわずかに破ってみよ。


課 題3に従い、安定性条件(S)をわずかに破った場合、次のような結果が得られます。
(S)の左辺の値が 0.51 と、1/2 をわずかに越えているにすぎませんが、最終的には真の解からはほど遠いギザギザの状態になってしまいます。
1/2 を大きく越えると、計算がおかしくなるのも早まります。
(ここではD をα>1/2 となるように調整した。)


上 の、ギザギザの解は、直感的に考えても熱方程式の解としてはおかしいことがわかると思います。(スプーンの熱分布がこの様になるとは思えません。)
陽解法においては安定性条件を満たさないパラメータで計算すると、計算が不安定となり、上記のような現象が見られます。
(ただし、これは、「差分方程式の解としては」正しいものです。しかし、近似したい熱方程式の近似解とはなっていところが問題なのです。)
注意:上の計算結果では対称性の崩れ(初期値は π/2 で対称)がみられます。これは、πを近似値として与えたためであると思われます。


この様に、陽解法においては、計算が安定に進むためには時間刻み幅および空間刻み幅が安定性条件(S)を満たさねばなりません。
ところが、この条件は計算量の点で問題があります。
例えば、時刻0から1まで計算を行うとします。
時間方向の格子点の総数は 1/τとなります。
計算の手間(計算時間)を減らす為には τ をなるべく大きく取りたい(τ が大きければ少ない繰り返し計算で目的の時刻まで計算できるので)。
そこで、安定性条件(S)より、
τ ≒ h2/(2D)
とすれば良いでしょう((S)を余裕をもって満たす τ を用いる)。
一方、D は一定とした場合、精度の良い計算をするために空間に関する分解能を高めたいとします。
そこで h を 1/10 倍にしたとします。すると、τ は上の関係から 1/100 倍せざるを得ません。
ということは、時間方向の格子点数が 100倍 に増えることになります。
これでは、 h を小さくしていったときに、あまりに格子点数が多くなりすぎて計算の効率が悪くなります。
例えば今の例では、h を 1/10 倍したため、計算量は 10 倍となり(これは仕方が無い)、更に安定性条件(S)を満たすために τ を 1/100 倍したので計算量は更に100倍となりました。
つまり、もとの計算量に比べて 1000倍の計算量が必要(1000倍時間がかかる!)となります。
つまり、陽解法では、空間解像度を細かくしたい場合に、時間刻み幅もそれにあわせて変更する必要があります。
できることならそういう事を気にせずに(全く気にしなくても良いわけではありませんが・・)自由に空間解像度を上げ下げしたいものです。
その要求を満たす解法として陰解法と呼ばれる方法があり、陰解法は無条件安定であることがわかっています(安定な計算に対するαに関する条件が無い)。
(演習では時間の関係から陰解法についてはできませんが、各人陰解法のプログラムの作成をしてみることをおすすめします。陰解法では連立一次方程式を解く必要がありますが、それらのプログラムは良いプログラミングの練習にもなります。)


レポート課題

次のノイマン条件下での拡散反応方程式について、

\[
\left\{
\begin{array}{l}
\frac{\partial u(x, t)}{\partial t} \ \ &= \ \ D \frac{\partial ^2 u(x, t)}{\partial x^2} \ \ \ \ \ \ \ \ \ \ \ \ \ \ (x \in (0, \pi), t \in (0,2))\\
u(x, 0) \ \ &= \ \ cos(x) + cos(7x) \ \ \ \ \ \ \ \ (x \in [0, \pi])\\
\frac{\partial u(0, t)}{\partial x} \ \ &= A\\
\frac{\partial u( \pi , t)}{\partial x} \ \ &= A\\
\end{array}
\right. \ \ \ \ \ \ \ \ (2)
\]  

拡散係数DD=0.1, 1.0, 10.0と三つの異なる場合について、同時にアニメーションに描画するpythonプログラムを作成し、Moodleから提出して下さい。
ただし、以下の点に注意すること

  • t=0から2.0までのアニメーション、A=0.001として、時刻tもwindow内に表示。
  • 異なる拡散係数を同時に表示するので、安定性条件に留意してすべての場合で計算が破綻しないように適切なパラメータを選択すること。
  • また、色の違いで拡散係数の違う拡散方程式の解を表示し、その情報も載せること。

採点基準はこれまで同様、コードの可読性、アニメーションの表示方法、必要要件の有無が採点対象となります。
インデント(時下げ)、コメントは必ず行うこと。
また、プログラムの最初の方にコメントとして学生番号と氏名は(最低限)必ず記述すること。
(# bxxxxxx 氏名)

提出期限:2023年6月2日(金)23時59分


おまけ1(反応拡散系の初歩)

(1) 初めに出てきた偏微分方程式をもう一度考えます.

(3) \begin{equation*} \left\{ \begin{array}{lc} \frac{\partial u(x, t)}{\partial t} = D\frac{\partial^2u(x, t)}{\partial t^2} + f(x, t) & (0 < x < \ell, t> 0) \\ u(0, t) = a, u(\ell, t) = b & (t>0) \\ u(x, 0) = u_0(x) & (0<x<\ell) \\ u_0(x) = a, u_0(\ell) = b & \end{array} \right. \end{equation*}

この式を
f(x, t) = 2x - x^3
L=1, D=0.1
境界条件: a = b = 0
初期条件: x \le Pならばu_0(x) = 1.0, x > P” height=”18″ width=”144″>ならば<img loading=
等とし、その様子を見よ(P をいろいろ変えてみよ)
(この式はGinzburg-Landau方程式と呼ばれるものです.)

計算数理A演習第12回

コンテンツ

第12回

連結バネ系

前回、前々回と偏微分方程式の数値解法について演習を行いましたが、今回は今一度オイラー法とルンゲクッタ法を用いた数値解法に取り組みます。
今回は、次のように x 軸上にバネを並べ、おもりでつなげた場合どのような運動をするか、数値的に計算してみましょう。
ただし、全てのバネの自然長 R (伸びも縮みもしていないときの長さ)とバネ定数が同じで、おもりの質量も同じとします。

左のおもりから順に 0, 1, …, M, M+1 と番号を振ります。
全てのバネが伸びも縮みもしていない状態であれば、m 番目のおもりの位置はバネの自然長 R を用いて、m*R と書けます。
これを用いると、m 番目(m ≠ 0, m ≠ M+1)のおもりの運動は、m*R からのズレ ym を用いて次のように書けます。

(1) \begin{equation*}\left\{ \begin{array}{l}y''_m(t) = -\omega^2(y_m(t) - y_{m-1}(t)) - \omega^2(y_m(t) - y_{m+1}(t)) = \omega^2(y_{m-1}(t) - 2 y_{m}(t) + y_{m+1}(t)) \\y_m(0)  = 0 \\y'_m(0) = 0\end{array}\right.\end{equation*}

0 番目と M+1 番目のおもりは固定されて動かないとします。つまり、

(2) \begin{equation*}\left\{ \begin{array}{l}y_0(t) = 0 \\y_{M+1}(t) = 0 \\y'_0(t) = 0 \\y'_{M+1}(t) = 0\end{array}\right.\end{equation*}

とします。

実際には,y_m(t) = y1_m(t), y'_m(t) = y2_m(t)とした, 2(M+2) 個の連立微分方程式に対し, オイラー法(ルンゲ・クッタ法)を利用します.

補足: 第6回のバネの伸びの方程式を応用すると式(1)がでてきます。
y_jをおもりの場所そのものとしても式(1)と同じ方程式になることと合わせて確認してみてください。

オイラー法でのアルゴリズムを下に示します。ルンゲ・クッタ法ではどうすればよいかは各自考えてみてください。


アルゴリズム

解を求める時刻の上限をTとし、適当な自然数Nを定めてh=T/Nとする。
(もしくは先にhを定めてN=int(T/h)とする。)
y1_mを配列変数 Y1[j][m] で,y2_mを配列変数 Y2[j][m] で表す.
(配列 Y1[N+1] [M+2], Y2[N+1][M+2]は実数型配列。)
T, h, t 及び 関数 f1(t, x), f2(t, x1, x2, x3) は実数型
M, N, j, m は整数型(int)


必要なライブラリの読込
T, h, M, N, R, ω (w 等で代用)を設定

Y1とY2をnp.zerosを用いてN+1行(時間), M+2列(質点ID)の2次元配列として初期化する

X0を1次元の自然長質点座標ベクトルとして定義
X0=[R, 2R, ・・・・・・・, MR]
(1. 初期値を与える)
Y2[0][1]=0.25(課題1での初期値の設定)
t = np.linspace(0, T, N+1)

m = 0, 1, 2,...., M+1 に対し初期値のグラフの描画(○と線)

f1とf2の定義

(2. 計算)
(時間方向への繰り返し)
j = 0,1,2,.......,N の順に (for 文)
    (全てのおもりについて計算) 
    m = 1,2,.......,M の順に (for 文)
        Y1[j+1][m] = Y1[j][m] + h*f1(t, Y2[j][m]) 
        Y2[j+1][m] = Y2[j][m] + h*f2(t[j], Y1[j][m-1], Y1[j][m], Y1[j][m+1]) 
        を繰り返す 
各時刻の質点座標をX1として計算(自然長座標X0 + 座標変位量Y1 )
X1 = X0+Y1

まずはグラフ化(ax.plotやpatchを使って必要なものを準備)
一定間隔毎に(例えば (j+1)%100==0 の時に, 等)
animation化 
set_data関数でデータの更新 
plt.pause関数を使ってアニメーション化
を繰り返す

f1(t, x), f2(t, x1, x2, x3) は関数として定義

描画の時間間隔やpause関数の時間を適度に調整して, 滑らかな動きが見えるようにしてください.


課題1

M=1、ω=1、R=1 として、式(1)と式(2)をとくプログラムを作成せよ。ただし初期条件は、y11(0)=0.0、y21(0)=0.25 とする。
オイラー法の場合, 時間刻み幅は h=0.0001 (T=300等)として、1000ステップ毎に、おもりの位置 (m番目のおもりの位置は (m*R + ym, 0))を ○で表示させること。

(ルンゲクッタ法の場合, 時間刻み幅は h=0.1 として、1ステップ毎に、おもりの位置 (m番目のおもりの位置は (m*R + ym, 0))を ○ で表示させること。)

次図ではグラフにする横軸の範囲は[ 0, 2 ]とした。
また下のアニメーションはファイルサイズ軽減のためT=31.4としているので注意。

課題2

課題1のプログラムを M = 3, 10 にしてその様子を見よ。y21=0.7、それ以外のy1m、y2mはゼロを初期条件とする。(グラフの横軸の範囲は [ 0, (M+1)*R ] とする.)


課題3

課題2のプログラムのグラフ表示部分を、点 (m*R, y1m) を直線で結ぶように書き換えよ。また, M をさらに増やした場合の様子を見よ. (グラフの縦軸の範囲は [ -0.5, 0.5 ] 等)

また下のアニメーションはファイルサイズ軽減のためT=31.4としているので注意。


課題4

課題3のプログラムをルンゲ・クッタ法を用いて書き直せ。時間刻み幅は h=0.1 として、1ステップ毎に、おもりの位置を表示させること。


課題5

M = 10、y21=0.7、それ以外のy1m、y2mはゼロを初期条件として、
次の二つの図を上下に表示させ、アニメーションを行うプログラムを作成せよ。
上の図:課題2と同様の図
下の図:課題3と同様の図
ただし、数値計算の方法はルンゲ・クッタ法であるとし、時間刻み幅はh=0.1であるとする。


小ネタ:連結バネ系について…

あらゆる物質は分子、原子(厳密には素粒子ですが)から出来ています。
そのうち「固体」と呼ばれているもの(氷等の結晶や金属等)は、そのような分子(原子)間に働くの引力と斥力によって、あたかもバネで繋がれたかのように、それらが規則正しく(等間隔に)配置されている状態であります。(この「バネ」という比喩は実際の分子によく合致します。)
ですので今回の課題2、3は、このような「固体」を原子レベルで見たような事になっています。(厳密には違いますが、大まかに…)
ω によってその「固体」の固さが変わる感じです。
課題2、3 では波のようなものが見えていたかと思いますが、この波が空気に伝搬して耳に入ると「音」として我々が認識することになります。
そういえば昔、量子力学用語の解説として「世の中〜波で出来ている〜」と踊っていた人がいたとかなんとか…

発展問題

今回の課題について、おもりに関するfor文を使わずに(ベクトル演算を使って)プログラムを書け。

小ネタ:ベクトル演算

例えば、M+2個の配列 y, z に対して

for m in range(1, M+1): 
    z[m] = (y[m-1]+y[m]+y[m+1])/3

として3近傍の移動平均を計算する場合、

z[1:-1] = (y[0:-2]+y[1:-1]+y[2:])/3

と書いてやれば、ベクトル演算となり、for文を使わなくてもよくなります。また、実は関数の仮引数はベクトルでも良く、また戻り値も同様です。

このようなベクトル演算は、プログラムの可視性を上げたり、並列計算をするときなどに効率がよくなるという利点もあります。(科学計算ソフトウェアのMATLABなどは、for文の実行速度が非常に遅く、ベクトル演算できる部分はベクトル演算にしないと非常に計算効率が下がります。)

また(この授業で散々Pythonを使って微分方程式を解かせておいてアレですが)、Pythonの最大の強みはデータ解析のライブラリが豊富なことです。集計や関連性の解析や機械学習など、手軽に様々な解析ができるようにライブラリが作られていますし、何より無料で使えます。
データもいわばベクトルや行列の形式です。ベクトル演算・行列演算ができればプログラムは非常にすっきりします。

一度作ったプログラムを見直し、ベクトル演算できるところを探してみてはどうでしょうか?

計算数理A演習第13回

コンテンツ

第13回

波動方程式の数値解法

今回は1次元波動方程式(wave equation)を扱います。
ここでは、1次元波動方程式をコンピュータを用いて解くために差分化を行い、コンピュータを用いて波動方程式を解くアルゴ リズムを示します。
今日の内容に関する詳しい情報は講義で説明があります(ました)。
演習では細かい事は気にせずに一気にアルゴリズムの紹介まで行きたい と思います。

さ て、天下り的ですが1次元波動方程式は弦の振動をあらわします。
(導出等は講義で説明されます。また前回の「連結バネ系」からも導出できます。時間があれば紹介します。)
下記方程式 (W) におけるu(x, t)は弦の変位をあらわします。
演習では1次元波動方程式を数値計算を用いて解き、その解u(x, t)を画面上にアニメーションとして表示する事を目標とします。
つまり、コンピュータ内部に弦の振動を再現し、それをアニメーションとして表示するわけです。

時間-空間2階偏微分方程式の差分化

さて、次の1次元波動方程式の初期値境界値問題、

\[
\left\{
\begin{array}{l}
\frac{\partial ^2 u(x, t)}{\partial t^2} \ \ = \ \ c^2 \frac{\partial ^2 u(x, t)}{\partial x^2} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (0 < x <l, t >0)\\
u(0, t) \ \ = u(l, t) \ \ = 0 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (t>0)\\
u(x, 0) \ \ = f(x), \ \ \ \ \frac{\partial u(x, 0)}{\partial t} \ \ = \ \ g(x) \ \ \ \ \ \ (0 < x <l)\\
\end{array}
\right. \ \ \ \ \ \ \ \ (W)
\]  

但し、$c>0, f(0)=0, f(L)=0, g(0)=0, g(L)=0$を考えます。
境界条件、初期条件が適切に与えられれば式(W)の解が弦の振動を表現していると言えるでしょう。
さて、数値計算する為にまず式(W)を離散化する必要があります。

これまでの演習で拡散方程式の数値解法を扱いましたが、そこでは時間方向の離散化として下図の(1)を、空間方向の離散化として下図の(2)を採用してきました。今回も同様です。

[0, L]区間をN等分するとし、上記離散化方法に従い、$h=L/N, \tau>0$とし、x_j = (j-1/2)h, t_k=k\tauとして、

\[ U_j^k \approx u(x_j, t_k) \ \ (j=1, \cdots, N, k=0, 1, \cdots) \]

と書くことにすると、(W)の第一式は次のように離散化されます。

\[ \frac{1}{\tau^2} (U_j^{k-1} - 2U_j^k + U_j^{k+1}) = \frac{c^2}{h^2} (U_{j-1}^{k} - 2U_j^k + U_{j+1}^{k})\ \ (j=1, \cdots, N, k=1, 2, \cdots) \]

\lambda = c\tau/hとして、まとめると

\[U_j^{k+1} = 2U_j^k - U_j^{k-1} + \lambda^2(U_{j-1}^{k} - 2U_j^k + U_{j+1}^{k})\]

という漸化式が得られます。
2階微分の差分化については、このページ最後のスライドを参照してください。

初期条件

初期条件も差分化する必要がありますが、拡散方程式のときと異なる点があります。
先ほどの離散化した漸化式において、次の時刻t_{k+1}でのUを計算するためには2つの時刻t_{k-1}及びt_kでのUが必要です。
したがって、漸化式を順次計算する上で、まずはU_j^0U_j^1が必要となります。
まずは(W)の第三式より、

\[ U_j^0 = f(x_i) \ \ (i=1, 2, \cdots, N) \]

となることは良いでしょう。ではU_J^1はどのように決めれば良いでしょうか?
U_J^1を決める方法はいくつか考えられますが、例えば次のようにします。
まず、テイラーの公式を用いると、

\[ u(x, \tau) = u(x, 0) + \tau\frac{\partial u(x, 0)}{\partial t} + \frac{\tau^2}{2} \frac{\partial^2 u(x, 0)}{\partial t^2} + O(\tau^3) \]

となります。ここで式(W)のまだ使っていない条件

\[ \frac{\partial u(x, 0)}{\partial t} = g(x) \]

を使います。さらにt=0でも(W)の第一式が成り立つとすると、

\[ \frac{\partial^2 u(x, 0)}{\partial t^2} = c^2 \frac{\partial^2 u(x, 0)}{\partial x^2} \]

となります。この式の右辺は2階差分商を用いて

\[\frac{\partial^2 u(x, 0)}{\partial x^2} \approx \frac{c^2}{h^2} \{ u(x-h, 0) - 2u(x,0) + u(x+h, 0) \} \]

と近似できます。つまり、

\[ U_j^1 = U_j^0 + \tau g(x_j) + \frac{\lambda^2}{2} (U_{j-1}^0 - 2U_j^0 + U_{j+1}^0) \]

と求まります。

境界条件

境界条件については拡散方程式のときと同様です。今回の(W)における境界条件は Dirichlet条件ですので、

\[ U_0^k = - U_1^k, U_{N+1}^k = - U_N^k \ \ (k=1, 2, \cdots) \]

となります。
他の境界条件の場合については、第10回を参考にして下さい。

解くべき(差分)方程式

以上の考察から解くべき式をまとめると、以下のようになります。

\[
\left\{
\begin{array}{l}
U_j^{k+1} \ \ = \ \ 2U_j^{k} – U_j^{k-1}+ \lambda^2(U_{j-1}^k-2U_j^k + U_{j+1}^k)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (j=1 \cdots, N, \ \ k=1,2,)\\
\frac{U_0^k+U_1^k}{2}=0, \ \ \frac{U_N^k+U_{N+1}^k}{2}=0 \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (k=1,2,\cdots)\\
U_j^0 \ \ = \ \ f(x_j) \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (j=1,\cdots, N) \\
U_j^1 \ \ = \ \ U_j^0 + \tau g(x_j) + \frac{ \lambda^2}{2}(U_{j-1}^0 -2U_j^0+ U_{j+1}^0) \\
\end{array}
\right. \ \ \ \ \ \ \ \ (2)
\]  

ただし、\lambda \le 1 \ (\lambda=c\tau/h)が安定性のための必要十分条件です。
拡散方程式では1/2が上限でしたが、波動方程式では違う値になります。
(安定性条件とは、数値計算がうまく行く為の条件ですなぜにこういった条件が必要であるかは、講義で説明があります)。

めでたく差分方程式が得られました。
(2)をコンピューターを用いて解くのが目標です。

アルゴリズム

あくまで、参考用です。始めに自分で解き方を考えてください。

1次元波動方程式を解く為のアルゴリズムは次のようになります。
安定性条件を満たすため\lambda \le 1となるようにh, \tauを与えます。
空間用の箱が何個必要なのかについては,第10回講義の拡散方程式の実装で紹介した
ルーチン図を参考に考えてください.

(1)初期パラメータ設定T(時間上限), N(時間刻み数), L(空間幅), M(空間刻み数), c を設定する
τ=T/N, h = L/M, λ = τ*c/h (λ ≦ 1 となるように. M, N を調整, プログラム中では, λは「lamb」, τは「tau」などとするとよい.) 

(2) 初期値設定
#必要な箱の準備
t_vec = #時間用1次元ベクトル
X_vec = #空間用1次元ベクトル
U=      #二次元配列(時間ID, 空間ID)

#初期値代入
U[0]=   #f(xi)の式
#境界条件:dilekli
U[0][0]= ???など

U[1]=   #テイラー展開から考えた式(ちなみにg(x) は課題1,2を参照)
#境界条件:dilekli
U[1][0]= ???など

(3) 更新計算
k := 1,.....,N の順に 
j := 1,.....,M の順に
    ??? = ???? #更新式を書く
    (境界条件:dilekli)
    u[k][0] = -u[k][1], u[k][M+1] = -u[k][M]


(4)動画描画
#グラフの準備
(x[k][0], u[k][0], x[k][M+1], u[k][M+1] は境界処理用なので表示しない)

k := 0,....,N の順に 
    (iii)描画 一定間隔毎に(例えば (j+1)%100==0 の時に, 等)
        (x[1], u[1]) ~ (x[N], u[N]) を画面に表示 #(x[k][0], u[k][0], x[k][M+1], u[k][M+1] は境界処理用なので表示しない)
を繰り返す

課題1

$L=1, c=0.1,\ \ f(x)=2x(1-x),\ \ g(x)=0,\ \ u(0,t)=\ u(1,t) =\ 0$として (2) を解くプログラムを作成せよ。
(N100程度、T \sim 100\lambda \le 1となるようにMを適度に選び, 描写頻度も適度に選ぶ事。)


課題2

$L=1, c=0.1,\ \ u(0,t)=\ u(1,t) =\ 0$(N = 100程度) はそのままで、f(x)およびg(x)を他のもにかえてみよ。例えば関数g(x)=0、関数f(x)

f(x) = \exp(-100(x-0.4)^2)

としたり

f(x) = \exp(-100(x-0.4)^2) - \exp(-50(x-0.7)^2) + \exp(-10(x-0.5)^2)

とする等。
(局在化した初期値からはじめると、波の伝播が観察される。)


課題3

境界条件を、Neumann境界条件や周期境界条件を適応したプログラムを作成し、実際に数値計算してみよ。

周期境界条件u(x,t)=u(x+1,t)のもと、進行波解\sin 2\pi (x-t)を実現するためには、どのようなパラメータで、どのような初期値から計算する必要が有るか。


発展課題1

弦の振動ではなく、膜の振動を考えたい。すなわち、1次元波動方程式ではなく2次元波動方程式を考える。どのような方程式になるであろうか?


発展課題2

2次元波動方程式を差分化し、プログラムを作成し、数値計算せよ。

Pythonでの3次元プロットの方法は各自調べてみてください。

補足:差分近似について

導関数の差分近似について

関数 u(x) を x のまわりでTaylor展開すると

(S1) \begin{equation*}u(x+d) = u(x) + du'(x) + \frac{1}{2}d^2 u''(x) + \frac{1}{6}d^3 u'''(x) + O(d^4)\end{equation*}

(S2) \begin{equation*}u(x-d) = u(x) - du'(x) + \frac{1}{2}d^2 u''(x) - \frac{1}{6}d^3 u'''(x) + O(d^4)\end{equation*}

となり、両辺を加えると、

(S3) \begin{equation*}u(x+d) + u(x-d) = 2u(x) + d^2 u''(x) + O(d^4)\end{equation*}

となります。O(d4)を無視すると、

(S4) \begin{equation*}u'' \approx \frac{1}{d^2} \{ u(x+d) - 2u(x) + u(x-d) \}\end{equation*}

この式の右辺の誤差はd^2程度となります。

展開すると

(S1) \begin{equation*}u(x+d) = u(x) + du'(x) + \frac{1}{2}d^2 u''(x) + \frac{1}{6}d^3 u'''(x) + O(d^4)\end{equation*}

(S2) \begin{equation*}u(x-d) = u(x) - du'(x) + \frac{1}{2}d^2 u''(x) - \frac{1}{6}d^3 u'''(x) + O(d^4)\end{equation*}

となり、両辺を加えると、

(S3) \begin{equation*}u(x+d) + u(x-d) = 2u(x) + d^2 u''(x) + O(d^4)\end{equation*}

となります。O(d4)を無視すると、

(S4) \begin{equation*}u'' \approx \frac{1}{d^2} \{ u(x+d) - 2u(x) + u(x-d) \}\end{equation*}

この式の右辺の誤差はd^2程度となります。