表計算ソフトのところですでにやったのと同じ計算を試みます。
さしあたって、答えがπになることはわかっています。
// プログラム numint.c #include <stdio.h> #include <math.h> int main(void) { int i,n; double x,fx,d,u,dx,sum; n=1000; // 分割数 d=0; // 積分の下限 u=M_PI; // 積分の上限 dx=(u-d)/n; // 分割の1区間 sum=0; for(i=0;i<n;i++) // 分割数だけ回す { x=d+i*dx; // i番目の区間のx fx=x*sin(x); // 積分する関数 sum=sum+fx*dx; printf("%4d %8.5f %8.5f %8.5f\n",i,x,fx,sum); } printf("数値積分結果 %15.13f (解析解 %15.13f)\n",sum,M_PI); return 0; }
$ gcc -o numint numint.c $ numint 0 0.00000 0.00000 0.00000 1 0.00314 0.00001 0.00000 2 0.00628 0.00004 0.00000 だ ぁ ぁ 998 3.13531 0.01970 3.14156 999 3.13845 0.00986 3.14159 数値積分結果 3.1415900697330 (解析解 3.1415926535898)以下、プログラムの説明です。
sum=sum+fx*dx; printf("%4d %8.5f %8.5f %8.5f\n",i,x,fx,sum); }の部分を
sum=sum+fx*dx; if(i%100==0) { printf("%4d %8.5f %8.5f %8.5f\n",i,x,fx,sum); } }と変えてみましょう。
![]() |
数値積分の誤差と計算方法 |
積分区間をN等分したとして、その区分点をx[0]〜x[N]とします。x[0]は積分区間の下限、x[N]は上限になります。また、その間隔をΔx(=(上限-下限)/N)とします。
(右図では、3等分してあって、x[0],x[1],x[2],x[3=N]に縦線が引いてある)。
また、x[i]に対する関数の値f(x[i])=f[i]と書くことにします。
上の、短冊を組み合わせた積分は、
です(f(x[0])Δx+f(x[1])Δx+...)。
それに対して、台形でつくるときは、
です。この、Σの中をよく見ると、
となっています。書き換えると、
よって、元々の台形積分は、
となります。先ほどの短冊を組み合わせた積分と見比べると計算に時間がかかりそうなひたすら合計する部分はほとんど変わらず(0...N-1が1...N-1になっただけ)、両端の1/2がくっついただけです。実質的に計算時間はほとんど変わりません。
ところが、この二つの積分は計算対象によっては劇的に精度に違いが出ます。
それを比較してみましょう。
台形積分は短冊の積分の結果に細工をすれば得られます。
※Σに0を含めて、その分を外で-f[0]、計算まとめ
この式を別に解釈すると、短冊の時は、短冊の左端を目的の曲線にあわせるか、右端にあわせるかすることでもう一方の端の値が無視されるのに対して、両端を半分ずつ反映しましょう、とも見られます。
※上のプログラムをコピーし、numint_dai.cに名前変更、
※//★の行に注意して修正すると、手っ取り早い。
※もちろん、「慣れる」には打ち直すのがよい。
// numint_dai.c #include <stdio.h> #include <math.h> #define f(x) ((x)*(x)) // ★追加 int main(void) { int i,n; double x,fx,d,u,dx,sum; n=1000; // 分割数 // ★1000に戻した? d=0; // 積分の下限 u=4; // 積分の上限 // ★修正 dx=(u-d)/n; // 分割の1区間 sum=0; // sumはΣの値の計算 for(i=0;i<n;i++) // 分割数だけ回す { x=d+i*dx; sum=sum+f(x); // ★式修正、printf削除 } printf("解析解: %15.12f\n",(u*u*u-d*d*d)/3); printf("数値積分(短冊):%15.12f\n",sum*dx); printf("数値積分(台形):%15.12f\n",(sum+(f(u)-f(d))/2)*dx); return 0; // ★↑3行:表示の修正 }
$ gcc -o numint_dai numint_dai.c $ numint_dai 解析解: 21.333333333333 数値積分(短冊):21.301344000000 数値積分(台形):21.333344000000 $プログラムはΣの部分をi=0〜n-1のforで繰り返しつつ求めたsumをもとに、最終的な計算はprintfに数値を渡す段階で行っています。
ここで、新しいキーワードが増えました。
#define f(x) ((x)*(x))これは、この先プログラム中に「f(?)」の形が出てきたとき、「((?)*(?))」に置き換えるように、という指示です。とりあえず、xが仮に書いてありますが、「x」の部分も書き換える、という意味しかありません。
ちなみに、しつこく括弧が書いてあるには訳があります。
#define f(x) x*x → f(a)はa*a、 f(a+1)は a+1*a+1=2a+1 #define f(x) ((x)*(x)) → f(a)は((a)*(a))、f(a+1)は((a+1)*(a+1))と、括弧がないと予期せぬ結果が起きやすくなります。 その側の括弧も、f(x)がx+3のような場合に重要になります。 括弧をつける癖をつけておきましょう。
なお、このdefineは
#define U 4のように、()なしでも使え、その場合は U が出るたび、4に置き換えられます。
もう、説明不要だと思いますが、 f(d)が上の式のf[0]、f(u)がf[N]にあたります。
試してみましょう
![]() |
バネとダンパによる振動 |
// simu1.c #include <stdio.h> #include <math.h> int main(void) { int i,n,nd; double x,v,a,m,c,k; double dt,end,iv; m=1.0; // シミュレーション対象パラメータ c=0.5; k=1.0; x=1.0; // xの初期値 v=0.0; // vの初期値 dt=0.001; // 計算区分 end=10; // 終了時間 iv=0.1; // 表示間隔 n=(int)(end/dt); // 計算繰返数 // "(int)"は「整数にする(切捨)」という指示。 nd=(int)(iv/dt); // 表示周期 for(i=0;i<n;i++) // 繰り返し { a=-(k/m)*x-(c/m)*v; x = x+v*dt; v = v+a*dt; if(i%nd==0) // nd回に1回表示 { printf("time:%6.3lf x:%6.3lf v:%6.3lf\n",i*dt,x,v); } } return 0; }これまでのプログラムより、少し長くなっているように見えますが、準備部分が増えていて、forのループ部分はあまり変わりません。
実行すると、
$ gcc -o simu1 simu1.c $ simu1 time: 0.000 x: 1.000 v:-0.001 time: 0.100 x: 0.995 v:-0.098 time: 0.200 x: 0.981 v:-0.190 time: 0.300 x: 0.957 v:-0.275 time: 0.400 x: 0.926 v:-0.354 time: 0.500 x: 0.887 v:-0.425 time: 0.600 x: 0.841 v:-0.489 : time: 9.300 x:-0.079 v:-0.041 time: 9.400 x:-0.083 v:-0.031 time: 9.500 x:-0.086 v:-0.021 time: 9.600 x:-0.087 v:-0.012 time: 9.700 x:-0.088 v:-0.003 time: 9.800 x:-0.088 v: 0.006 time: 9.900 x:-0.087 v: 0.014 $という結果が出てきます。
![]() |
バネとダンパによる振動 |
: time: 1.700 x: 0.118 v:-0.674 time: 1.800 x: 0.052 v:-0.649 xが初めて time: 1.900 x:-0.011 v:-0.619 負になるのは time: 2.000 x:-0.072 v:-0.585 1.9秒くらい : time: 3.100 x:-0.440 v:-0.066 time: 3.200 x:-0.445 v:-0.019 xのマイナスの time: 3.300 x:-0.444 v: 0.025 局地は-0.445 time: 3.400 x:-0.440 v: 0.067 あたり : time: 9.800 x:-0.088 v: 0.006 time: 9.900 x:-0.087 v: 0.014 10秒あたり右のグラフと見比べると、傾向は同じようです(でなければおかしい)。
ここで、表計算の時の計算誤差で問題になった、c=0にしたときを見てみます。 本来は、xの揺れ幅は大きくなるはずはありません。が、
time: 0.000 x: 1.000 v:-0.001 time: 3.100 x:-1.001 v:-0.041 time: 6.300 x: 1.003 v:-0.018 time: 9.400 x:-1.004 v:-0.024と、わずかずつ増えています。これでも表計算の時よりは、計算が細かくなった分だけ、ましになっています。
time: 0.000 x: 1.000 v:-0.000 time: 3.100 x:-0.999 v:-0.042 time: 6.299 x: 1.000 v:-0.016 time: 9.399 x:-1.000 v:-0.026振幅は増えなくなりました。 3.1秒のところで0.999となっているのは、3.1秒のそば(3.13あたり)で1.000になっているけど表示がとばされているだけ、3.1のときに0.999だったということです。
やってみよう
if(v>0) { a=(-kx-F)/m; } else { a=(-kx+F)/m; }という書き方でaを決めれば、速度の方向で加速度を変えられます。
表計算の最後の課題とすっかり同一条件でやってみましょう。
振り子は、振り子の向いている角度θ(真下で0,真横で±π/2[rad])について、
または、
(単に角速度ωを用意して、分解しただけ)
という関係が成り立ちます。
この振り子の運動をシミュレートしてください。
なお、振り子の初期角度は「80+学籍番号下一桁[deg]」(80〜89)、振り子の長さは「3+学籍番号百の位」(4or5)でつくってください。
計算の時間間隔(dt)はお任せ、ただし、結果は「0.1秒(くらい)間隔、10秒間の計算結果」を「時刻、角度」の組で表示するようにしてください(角速度などが表示されても可、単位もお任せ)。
なお、まえのレポートの結果と比較してみるとよいでしょう。
異なる場合は、どちらかがなにか違います。
※「θ、ω」は変数名に使えないため、theta, omega などを使用。
※角度はdegからradに変換しないといけない。[rad]=[deg]/180.0*M_PI;
※例:theta=85.0/180.0*M_PI;
課題番号r05にて12/7までに提出
ヒント:
そこで、グラフ化やデータの扱いについては表計算に任せてみましょう。
計算結果を表計算ソフトに入れることは簡単です。
まずやってみましょう。
// simucsv.c #include <stdio.h> #include <math.h> int main(void) { int i,n,nd; double x,v,a,m,c,k; double dt,end,iv; m=1.0; // シミュレーション対象パラメータ c=0.5; k=1.0; x=1.0; // xの初期値 v=0.0; // vの初期値 dt=0.001; // 計算区分 end=10; // 終了時間 iv=0.1; // 表示間隔 n=(int)(end/dt); // 計算繰返数 // "(int)"は「整数にする(切捨)」という指示。 nd=(int)(iv/dt+0.5); // 表示周期 printf("time[s],x[m],v[m/s]\n"); // ★追加★ for(i=0;i<n;i++) // 繰り返し { a=-(k/m)*x-(c/m)*v; x = x+v*dt; v = v+a*dt; if(i%nd==0) // nd回に1回表示 { //printf("time:%6.3lf x:%6.3lf v:%6.3lf\n",i*dt,x,v); printf("%6.3lf, %6.3lf, %6.3lf\n",i*dt,x,v); // ★printfを修正★ } } return 0; }プログラム自体は、シミュレーションのところのバネダンパのシミュレーションのプログラムそのものです。 ただし、「★」の付いているところだけ修正してあります。
$ gcc -o simucsv simucsv.c $ simucsv time[s],x[m],v[m/s] 0.000, 1.000, -0.001 0.100, 0.995, -0.098 0.200, 0.981, -0.190 0.300, 0.957, -0.275 0.400, 0.926, -0.354 0.500, 0.887, -0.425 0.600, 0.841, -0.489 : 9.500, -0.086, -0.021 9.600, -0.087, -0.012 9.700, -0.088, -0.003 9.800, -0.088, 0.006 9.900, -0.087, 0.014 $と表示されます。変わったことは、最初に足したprintfの表示と、あとの修正でやたらと表示が質素になって見た目はわかりにくくなりました。
![]() |
CSVファイルをExcelで開いたところ |
![]() |
グラフにしてみた |
$ simucsv > simu.csv $としてください(これも全部半角、">"の右に注意。間違ってsimucsv とか simucsv.c とかすると、それらのファイルが消えてなくなります。".csv"が重要)。
できたファイルをフォルダで確認すると、Excelのらしいアイコンが付くので、ダブルクリックして開いてみましょう。
すると、Excelが起動し、さっき画面に表示されていた値がきれいにセルに入ります。
あとは以前と同じで、数値の範囲を選択して、グラフにしてみればOKです。
このように、
「>」をつかうリダイレクトは、Cygwinを含むUNIX関係のOSや、MacOSXの端末、Windowsのコマンドプロンプトなど、あちこちで使えます。使えないときは、プログラムで直接ファイルに書き出す、という操作が必要になりますが、とりあえず、この講義では避けておきます。
C言語の参考書にはふつうは載ってますので参考にしてみてください(printfのかわりにfprintfを使うようになる)。
表計算ソフト(や、CSVを出力するその他のプログラム)から、C言語のプログラムに値を持ってくることもできます。
使い道としては、CSVのデータを直接プログラムで処理したり、表計算ソフトなどでシミュレーション条件をいくつも決めた上で、C言語のプログラムに各条件でシミュレーションさせ、さらに結果を表計算に戻す、といったことも考えられます。
さて、そのためには、文字で書かれたファイルの内容を解釈しなければなりません。 しかし、このためのプログラムは、今までの数値計算とはまた別のC言語的知識(ファイルの読み込み、文字列操作)が必要となり、またプログラムも長いものとなります。
そこで、CSVファイルを読み込む部分は予め用意しました(ある意味、printfなども、標準的に用意されているもの)。それをつかって試してみましょう。
CSVファイルは、表計算で最小自乗法のときのデータをそのまま使います。細かい操作はこのページも参考にしてください。
(デスクトップの)「マイコンピュータ」→
(ネットワークドライブ)「Users(S:)」→
「Kumagai」フォルダ→
「Computer」フォルダを開いてください。
ここにある、csv.cを自分のcygwin/program フォルダにコピーしてください。
// csvtest.c #include <stdio.h> #include "csv.c" // ".c" に注意 // CSVファイルを読み込み値を得る LoadCSVとValueを含んでいるファイル int main(void) { LoadCSV("lmsdata.csv"); printf("B1:%lf\n",Value(EXB,1)); printf("C2:%lf\n",Value(EXC,2)); printf("A1:%lf\n",Value(EXA,1)); return 0; }
$ gcc -o csvtest csvtest.c $ csvtest ファイル 'lmsdata.csv' のサイズ:629 ファイルを読み込みました ファイルの行数:29 B1:28.000000 C2:14.769000 セルA1は数値と認識できません A1:0.000000 $このcsv.cには、CSVファイルを読むためのプログラムが入れてあります。 #include することで、それを自分のプログラムに取り込むことができます。
「LoadCSV("ファイル名.csv");」 はcsvファイルを読み込みます。 エラーがあればエラーが表示され、問題なければ、「ファイルの行数:」と表示がでます。
「Value(EX?,?)」は読み込み済みのcsvファイルの特定のセルに当たる数値を取得します。
セルを表す「A1」などの「A〜Z」を「EXA〜EXZ」で、行番号を数字で指定します。
例:セル「C5」はValue(EXC,5)
なお、元のファイルに存在しないセルを指定したり、あっても数値以外が書き込まれている場合(ここまでの操作通りなら、セルA1は「データ数」という数字ではない文字)、メッセージを表示して、値はゼロになります。
// lms.c #include <stdio.h> #include "csv.c" // ".c" に注意 // CSVファイルを読み込み値を得る LoadCSVとValueを含んでいるファイル int main(void) { int n,i; double x,y,sx,sy,sxx,sxy; double D,a,b; LoadCSV("lmsdata.csv"); sx=sy=sxx=sxy=0; n=Value(EXB,1); // B1セルから printf("データ数は%d\n",n); for(i=0;i<n;i++) { x=Value(EXB,2+i); // i=0...n-1, データは2行から 2+i=2,3,4... y=Value(EXC,2+i); sx=sx + x; sy=sy + y; sxx=sxx + x*x; sxy=sxy + x*y; } printf("SX: %lf SY: %lf SXX: %lf SXY: %lf\n",sx,sy,sxx,sxy); // 上の行はなくても支障はない. D=n*sxx-sx*sx; a=(n*sxy-sx*sy)/D; b=(sxx*sy-sx*sxy)/D; printf("y=%lf x + %lf\n",a,b); return 0; }
$ gcc -o lms lms.c $ lms ファイル 'lmsdata.csv' のサイズ:629 ファイルを読み込みました ファイルの行数:29 データ数は28 SX: 408.074000 SY: 3619.859000 SXX: 7818.105476 SXY: 68657.904847 y=8.499985 x + 5.401294 $最小自乗法の説明そのものは、以前の解説を参照してください。計算は
です。このプログラムでは、計算に使う
をそれぞれ、sx, sy, sxx, sxy という変数にして、最初にゼロを入れ、データの数だけforで加えています。 データの個数は1行目、B2のところにありますので、それを使っています。以前の表計算でやったときの結果と見比べ、同じ値が得られたか確認してみましょう。
やってみよう
本当の意味の乱数は、なんの関係もなく毎回の値が出てくるものですが、C言語で使う乱数は一般に「疑似乱数」と呼ばれている、ある数式で計算される値です。 見た目はランダムに見えますが、厳密には、非常に長い周期を持った繰り返しの数列です。 ただ、たいていの場合は、乱数としてみて、あまり問題はおきません。
乱数を使うには、ただ「rand()」とします。「rand()」が使われるたび、異なる乱数が得られます。
// rand.c #include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { int i,r; srand(1225); // 「乱数の種」を固定値で指定 //srand(time(NULL)); // 「乱数の種」を現在時で指定 printf("RAND_MAX=%d\n",RAND_MAX); for(i=0;i<10;i++) { r=rand() % 6 + 1; // randは0〜RAND_MAX, % 6 することで 0〜5に // +1することで1〜6にする。 printf("%d ",r); } printf("\n"); return 0; }
$ gcc -o rand rand.c $ rand RAND_MAX=2147483647 2 1 3 5 6 2 6 6 3 3 ※乱数なので状況で表示が変わる可能性大 $乱数を得るための関数「rand()」は、0〜RAND_MAX までの数値の範囲でランダムに値を出します。 このRAND_MAXという値は、コンピュータやOSなどによって変わることがありますが、さしあたって非常に大きい数値です。 今回はさいころとして使うため、ほしい数値は1〜6です。そこで、
なお、厳密には、このさいころには偏りがでます。
0を含めてRAND_MAXまで出る種類はRAND_MAX+1=2147483648種類の数値がでることになります。
これは357913941×6+2なので、さいころにするときに4種の目が357913941回、2種が357913942回でることになります(%6+1なので、前者が3〜6、後者は1,2)。
厳密には偏りですが、実際のさいころよりは遙かに高精度なので気にする必要はありません。
(一方で、微妙な周期性が問題になることはあり)。
プログラムの先頭のほうの「srand()」は「乱数の種」と呼ばれる、初期値設定するものです。
乱数に初期値?というのも変ですが、前述の通り、周期性のあるものなので、スタート地点が設定できます。
ランダム性を重視するなら、たとえば現在時刻(time()である起点からの秒数が得られる)のような、実行時で変化するような値を種として使います。
その一方で、「ランダムだけど、再現性のある確認をしたい」という場合は、敢えて特定の適当な数字を指定します。
(srandを使わない場合は、自動的に1がつかわれたりするようです。)
上のプログラムは1225を種にしているので、何度実行しても表示される乱数の順序は変わりません。
1225を1224に変えて再コンパイルすれば、別の順番で表示されますが、それも2回目の実行で変化ありません。
コメントにしてある、srand(time()); にすれば、実行ごとにかわるでしょう。
(ただし、1秒以内に2回実行すると同じになる可能性がある)
いずれにせよ、擬似的ですが、見た目それっぽい乱数が得られます。
実際に、乱数をつかって、コンピュータの中で実験をしてみます。
正しいさいころは、それぞれの目が出る確率は1/6です。
n回さいころを振ったら、n/6回、特定の目が出るはずです。
これを試してみましょう。
// rand2.c #include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { int i,ok,ng,r; srand(1225); // 「乱数の種」を固定値で指定 ok=0; ng=0; for(i=0;i<100000;i++) { r=rand() % 6 + 1; if(r==3) { ok++; } else { ng++; } } printf("OK:%d 回 NG:%d 回 OKの率:%12.8f %%\n", ok,ng,(100.0*ok)/(ok+ng)); return 0; }
$ gcc -o rand2 rand2.c $ rand2 OK:16639 回 NG:83361 回 OKの率: 16.63900000 % ※乱数なので状況で表示が変わる可能性大 $このプログラムでは、変数 i のforループによって、何回、さいころを振るかを決めています。
結果は、期待通りなら、16.666...%になるはずですが、微妙に差がでます。
この差は、前述の乱数の偏りというよりは、ランダムだからこそ生じた偏りです。
srandをtime()で行ったり、別の数字に変えてみると、16.66より多くも少なくも変わります。
さいころ1個だと実験するまでもなく、予想数値がわかるので、すこし面倒にしてみます。
「さいころを2個振って、その合計が5になるのは?」
// rand3.c #include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { int i,ok,ng,r1,r2; // ★変更 srand(1225); // 「乱数の種」を固定値で指定 ok=0; ng=0; for(i=0;i<100000;i++) // ★適当に大きな数<大きすぎると終わらない { r1=rand() % 6 + 1 ; // r1=1〜6 ★ここ2行も変更 r2=rand() % 6 + 1 ; // r2=1〜6 if(r1+r2==5) // ★変更 { ok++; } else { ng++; } } printf("OK:%d 回 NG:%d 回 OKの率:%12.8f %%\n", ok,ng,(100.0*ok)/(ok+ng)); return 0; }
$ gcc -o rand3 rand3.c $ rand3 OK:11130 回 NG:88870 回 OKの率: 11.13000000 % ※乱数なので状況で表示が変わる可能性大 $先ほどのプログラムと変わったところは、冒頭の変数を用意するところ、さいころを振るところ、条件です。
さて、見たところ、11%くらいですが、実際の期待値も考えてみます。
さいころを2個振ると、それぞれ1〜6がでますが、合計が5になる組み合わせは
(1,4)(2,3)(3,2)(4,1)
の4通りです。全体では6通り×6通り=36通りの組み合わせが等しく出る可能性があるため、
4/36=1/9=11.111..%
が想定される値です。
というわけで、それらしい値が得られました。
このように、「考えればわかるけど、本当に考えたことが正しいか」「考えてもわからなさそう」というときに、おおざっぱにチェックできるのが、コンピュータで乱数、の便利なところです。
なお、今回の程度のチェックでは、実際には、「全組み合わせをチェック」することもでき、その方が精度良く結果がわかります(さいころ3個で6×6×6=216通りにしかならない)。ただ、「さいころを20個ふって合計が○○になる」といった場合は、すべての組み合わせが6の20乗になるため、チェックしきれません。そういうときに非常に効果があります。
rand3.cあたりを改造して、以下のようなプログラムをつくり、 レポートr06 で、プログラムとその実行結果(今まで通りのセット)を提出して下さい。
問題:
さいころを3こ振って、合計が10以下となる確率を求めてください。
ヒント: