散乱波の足し算: Total Sum of Scattering Waves.

高分子科学専攻  講師 川口辰也 (revised 2026.03.25)

Cプログラムの改定とページの微修正

ChatGPTとGeminiにプログラムの改良を相談してみると,まあ大体同じことを指摘されたので修正してみると...
散乱体の行列というか,並びを作る部分が簡潔に。
あれ,そうすると格子ベクトルのなす角を90度に固定にせずにパラメータとして入れるのも簡単やね。じゃ,入力部分に角度を入れようか。
他にもループ内での計算を減らすとか構造体の配列ではなく配列の構造体にしたほうが速く動くなどなど色々なアドバイスが。 手を加えだしたら面白くなってきてしまった。

以前の内容はこちら

はじめに

日本学術振興会の小・中・高校生のためのプログラム「ひらめき★ときめきサイエンス」に参加して

昔の話になるのですが,阪大理学部の化学科は毎年このプログラムに参加して,8月の頭に高校生対象の 一日体験入学を開催していました。
筆者は2017年度と2018年度にX線回折の話で参加した。「原子で散乱された波の足し算を観測しているんだよ!」と高校生に実感して貰いたく… 波の足し算をするプログラムをC言語で作成した。

まあ,数十万円するソフトウェア(つまりMathematicaとか)ならすぐ式を書いて計算できるだろうし,無料のソフトでも探したら適当なものがあるだろう,たぶん。
でも,痒いところに手が届かないだろうし,動作がそんなに速くないだろうし,適当なソフトを探して使い方を把握して段取りを決めて…と掛かる時間を考えると(この程度なら)作る方が早い。

結果できたプログラムソース(改定前)...を生成AIのアドバイスで少しブラッシュアップしたものを公開する。 不具合が有っても責任は取れませんが好きに使ってください。拙いのはご容赦ください。

どんなことをするプログラムか

XY平面をX方向に進行する平面波が,原点付近を中心とする(それぞれ一個は等方的に散乱する)散乱体の行列に入射したときに発生する散乱波の振幅を計算する。 入射波は波長1.0の余弦波とする。入射波の位相,散乱体の行列の情報(a軸方向とb軸方向の個数,それぞれの間隔,格子ベクトル(ab)のなす角, 格子の回転角(a軸のX軸からの角度))と散乱振幅を計算するグリッドの情報(Xの下限・上限・刻み個数,Yの下限・上限・刻み個数)を与えることにより, グリッドの座標(X,Y)とそこでの散乱振幅(Z)のデータファイルとグリッドの座標(X,Y)とそこでの散乱体の形状(Z:半径は0.3とする)のデータファイルの2つを出力する。
データファイルをグラフにすると上の図のようなものを描ける。条件を少しずつ変えたグラフをたくさん描けば動画(2.1MB)も出来る。

必要とする環境

Mac,あるいはWindowsまたはLinuxの動くPC(64bit)とそれぞれにおける開発環境(C言語)
古いバージョンではコンパイルしたバイナリを用意してありましたが,今回はソースのみの提供ですので...

座標の計算だけでよい場合

シェルスクリプトを利用して波の動画を作る場合

使用法

Unixでの使い方を標準として記述する。

プログラムの設置

作業用のディレクトリを準備し,そこに計算プログラムのバイナリ「a.out」を置く

下拵え Cのソースファイルから自分でバイナリを作成

プログラムのソースファイルは「wave_2.5.c」である。一般的なC言語で記述してあるので適当なCコンパイラ(GCCを標準として考えてはいるが…)でコンパイルして実行ファイルを作成する。 GCCでコンパイルする場合は「gcc -O3 -ffast-math -march=native -fopenmp wave_2.5.c -lm」と入力すれば実行ファイル「a.out」ができる。
MacでXcode + libompだと「cc -Xpreprocessor -fopenmp -lomp -I"$(brew --prefix libomp)/include" -L"$(brew --prefix libomp)/lib" wave_2.5.c」となるらしい。

命令

その前に記述の仕方の確認

端末を起動してbashが入力待ちの状態では,設定によって変わるが,「$」がプロンプトの前に表示されていることが多いようだ。そこで

$

で入力待ちの状態を,そして

$ ls ⏎
Concat_FFmpeg.sh  PCについて.txt   a.out            wave.c          使い方.txt
MakeGraph.sh      PCについて.txt~  template.gplt    wave.c~         端が入ったグラフ.png

で「ls」を入力し,ファイルのリスト(Concat_FFmpeg.sh以下)が表示されたところを示すことにする。ほぼ筆者の環境での端末での表示そのままである。gnuplotを起動したときは

$ gnuplot ⏎

    G N U P L O T
    Version 5.0 patchlevel 3    last modified 2016-02-21 

    Copyright (C) 1986-1993, 1998, 2004, 2007-2016
    Thomas Williams, Colin Kelley and many others

    gnuplot home:     http://www.gnuplot.info
    faq, bugs, etc:   type "help FAQ"
    immediate help:   type "help"  (plot window: hit 'h')

Terminal type set to 'wxt'
gnuplot> 

となるので,「>」をgnuplotの入力待ちの状態を示すこととする。記述の仕方の確認は以上。

命令

$ ./a.out (ここに引数を並べる) ⏎
    →  ファイル「out_ScrtWave.dat」(散乱波のデータ)と「out_Atom.dat」(散乱体のデータ)を書き出す

「a.out」に引数をつけて起動すると散乱波と散乱体の2つの座標データが出力される。

散乱波を計算するのに必要なパラメータは以下の項目を順番に並べる。ここにある値は例である。

位相情報(νtの値)nu_t 実数 0.0
散乱体のa軸方向個数(a)n_a 整数 10
散乱体のb軸方向個数(b)n_b 整数 7
散乱体の間隔(a)l_a 実数 1.2
散乱体の間隔(b)l_b 実数 1.0
a,b軸のなす角(°単位)gamma 実数 90.0
散乱体行列の回転角(°単位)phi 実数 30.0
観測範囲(X下限)x_l 実数 -10.0
観測範囲(X上限)x_u 実数 30.0
観測範囲(Y下限)y_l 実数 -15.0
観測範囲(Y上限)y_u 実数 15.0
観測分割数(X)t_x 整数 800
観測分割数(Y)t_y 整数 600

上記のパラメータで計算をする場合は

$ ./a.out 0.0 10 7 1.2 1.0 90.0 30.0 -10.0 30.0 -15.0 15.0 800 600 ⏎

これで「out_SctrWave.dat」と「out_Atom.dat」の2つのファイルが出力される。 オプションとして文字列を追加すると出力ファイル名の「out」の部分が置き換わるので,

$ ./a.out 0.0 10 7 1.2 1.0 90.0 30.0 -10.0 30.0 -15.0 15.0 800 600 test ⏎

とすると「test_SctrWave.dat」と「test_Atom.dat」の2つのファイルが出力される。 これらの「*_SctrWave.dat」および「*_Atom.dat」にある座標データをgnuplotなど,グラフ作成ソフトで表示させれば散乱体と散乱波の関係が分かりやすい。

データファイル

「*_Atom.dat」および「*_SctrWave.dat」は

X0 Y0 Z0
X1 Y1 Z1
X2 Y2 Z2
X3 Y3 Z3
X4 Y4 Z4
...

と座標の値がフォーマット%9.5f %9.5f %9.5fで書き込まれたテキストファイルである。

gnuplotでの表示

散乱体データの床レベルを-1.1にしているため,gnuplotではzrangeの下限を-1.0以上にしておくと良い。

> set zrange [-2.0:3.0] ⏎
> splot "out_Atom.dat" w l ⏎

とした場合と

> set zrange [-1.0:3.0] ⏎
> splot "out_Atom.dat" w l ⏎

とした場合の図を並べる。

> set autoscale ⏎
> set zrange [-1.0:3.0] ⏎
> set view 15.0, 20.0 ⏎
> splot "out_Atom.dat" w l , "out_SctrWave.dat" w l lw 0.1 ⏎

「set view」はグラフの方位・スケールの指定。何度でも設定できる。 「set view 「rot_x」, 「rot_z」, 「scale」, 「scale_z」」と入力する。rot_z以降は省略可。{{{縦軸方向にscale_z倍してから}全体でscale倍し,}縦軸方向にrot_z回転してから}横軸方向にrot_x回転した状態で表示される。
「splot」は3次元グラフ表示の命令。引数としてファイル名あるいは関数,オプションを続ける。 「w l lw 0.1」は「w[ith] l[ines] l[ine]w[idth] 0.1」の省略形。線でデータ点を繋ぎ,線の太さを0.1 pointとする。「l」以外に「l[ines]p[oints]」や「p[oints]」,「lw 数値」以外に「l[ine]c[olor] 整数値」,「p[oint]t[ype] 整数値」,「p[oint]s[ize] 数値」 等が指定できる。

綺麗な図にするために一手間掛ける

上のデータを用いて実際にgnuplotで線を使って表示すると端のデータ点を繋ぐ線が入ってしまう。 その不具合を解消するために,xの範囲が-10.0~30.0,分割数が800とすると,データの間隔が0.05になるので, それを元に範囲の両端に二つずつデータ点を増やして分割数に4(2+2)を加える。 つまり範囲を-10.1~30.1にして分割数を804とする。yも同様にして

$ ./a.out 0.0 10 7 1.2 1.0 90.0 30.0 -10.1 30.1 -15.1 15.1 804 604 ⏎

としてデータファイルを作成する。そしてgnuplotではxrangeとyrangeをデータの両端を切るように指定する。

> set xrange [-10.0:30.0] ⏎
> set yrange [-15.0:15.0] ⏎
> splot "out_Atom.dat" w l , "out_SctrWave.dat" w l lw 0.1 ⏎

上の図と比較すると分かるように端のデータ点を繋げる線が入ることを防ぐことができる。 データの計算の入力の時点で素直でない数値を入れることになるが,この方法でgnuplot上で綺麗に表示が出来る。

バッチによるデータ処理

例えばnu_tを少しずつ変えた1周期分のデータからgnuplotで画像ファイルを作成し,パラパラ漫画の要領でffmpegを使って動画を作成することを考える。 一つ一つコマンドを入力するという手間が掛かることなどするものではない。このようなものはバッチ処理にする。 以下のようなスクリプトを作成して1°ずつ散乱体の格子の向きを変えた計算とグラフの作成,動画の作成を一気にさせることができる。bashやzshで使える。

シェルスクリプト MakeGraph2.sh パラメータは適当に編集して使うこと
書き換えたパラメータでいきなりフルに走らせず,まずは「endj=90」を「endj=0」とし,「rm out???.png」を「# rm out???.png」とコメントアウトする, 等のループを減らす・記録を残す手はずをして動作を確認するのが良い


#!/bin/bash

NU_T=0.0
N_a=10
N_b=10
L_a=2.4
L_b=1.5
GAMMA=90.0
PHI=0.0
X_L=-20.2
X_U=40.2
Y_L=-30.2
Y_U=30.2
T_X=604
T_Y=604

j=0
endj=90

while [ $j -le $endj ]; do

    PHI=`echo $j \+ 0.0 |bc`
    GRAPH=$(printf "%dx%d-%04.1fdegree" $N_X $N_Y $PHI)

    i=0
    endi=19

    while [ $i -le $endi ]; do

        NU_T=`echo $i \* 0.05 |bc`
        COMMAND=$(printf "./a.out %f %d %d %f %f %f %f %f %f %f %f %d %d out%02d" $NU_T $N_a $N_b $S_a $S_b $GAMMA $PHI $X_L $X_U $Y_L $Y_U $T_X $T_Y $i)
        $COMMAND
        COMMAND2=$(printf "sed s/OUTFILE/out%02d/" $i)
        COMMAND3=$(printf "sed s/INFILE/out%02d/g" $i)
        COMMAND4=$(printf "sed s/GRAPHTITLE/%s/" "$GRAPH")
        cat template.gplt | $COMMAND2 > TEMP1.gplt
        cat TEMP1.gplt | $COMMAND3 > TEMP2.gplt
        cat TEMP2.gplt | $COMMAND4 > COM.gplt
        gnuplot <COM.gplt
        rm TEMP?.gplt COM.gplt
        i=$(($i+1))
    done

    IPHI=$(echo $PHI |sed 's/.0$//')
    OUTFILENAME=$(printf "Wave%dx%d-%03d.mp4" $N_X $N_Y $IPHI)
    COMMAND0='ffmpeg -r 24 -i out%02d.png -an -c:v libx264 -vf format=yuv420p -tune animation '
    COMMANDV=$(echo "$COMMAND0" $OUTFILENAME)
    $COMMANDV
    rm out??.png

    j=$(($j+1))

done

gnuplotスクリプト template.gplt これもパラメータは適当に編集すること
MakeGraph.shにより出力するファイル名が置換されたものがgnuplotに読み込まれる。


set view 15.0, 20.0
set zrange [-1:2.7]
set xrange [-20.0:40.0]
set yrange [-30:30]
set title "GRAPHTITLE"
set output "OUTFILE.png"
set terminal pngcairo enhanced size 1200, 900
set key font 'Helvetica, 18' spacing 4
set title font 'Helvetica, 28'
splot "INFILE_Atom.dat" title "Atom" w l, "INFILE_SctrWave.dat" title "Scattering Wave" w l lw 0.1

上の例のスクリプトであれば,ターミナルで

$ bash MakeGraph2.sh ⏎

とすれば,散乱体の直交格子(10x10,間隔2.4,1.5)が0°から90°まで1°ずつ回った散乱の様子を示すビデオ(Wave10x10-000.mp4からWave10x10-090.mp4まで)が作成される。 それぞれの角度で波の1周期分になっている。

作成例

詳細

コンパイル

浮動小数点の計算でループが一杯で…最適化が露骨に効いてくるプログラムである。
コンパイラが最適化しやすい書き方をするのも重要とのことで,今回はそこにも気を使っている。
数値の厳密さをそれほど求めるものでもないので最適化は掛けられるだけ掛けるのが吉。

Ubuntu 20.04LTS,gcc 9.4.0,Core i7-6820HQの場合
最適化を掛けないと


$ gcc -O0 wave_2.5.c -lm ⏎
$ time ./a.out 0.0 15 15 1.2 1.0 90.0 30.0 -30. 40. -30. 30. 700 600 ⏎

real	0m4.929s
user	0m4.845s
sys	0m0.032s

よく使う-O3だと

$ gcc -O3 wave_2.5.c -lm ⏎
$ time ./a.out 0.0 15 15 1.2 1.0 90.0 30.0 -30. 40. -30. 30. 700 600 ⏎

real	0m3.658s
user	0m3.553s
sys	0m0.040s

最適化無しでOpenMPを有功にして

$ gcc -O0 -fopenmp wave_2.5.c -lm ⏎
$ time ./a.out 0.0 15 15 1.2 1.0 90.0 30.0 -30. 40. -30. 30. 700 600 ⏎

real	0m1.483s
user	0m5.836s
sys	0m0.068s

-O3でOpenMPを有功にして

$ gcc -O3 -fopenmp wave_2.5.c -lm ⏎
$ time ./a.out 0.0 15 15 1.2 1.0 90.0 30.0 -30. 40. -30. 30. 700 600 ⏎

real	0m1.316s
user	0m4.544s
sys	0m0.052s

OpenMPを無効として最適化を掛けられるだけ掛けると

$ gcc -O3 -ffast-math -march=native wave_2.5.c -lm ⏎
$ time ./a.out 0.0 15 15 1.2 1.0 90.0 30.0 -30. 40. -30. 30. 700 600 ⏎

real	0m1.221s
user	0m1.125s
sys	0m0.036s

OpenMPを有効とし,最適化を掛けられるだけ掛けると

$ gcc -O3 -ffast-math -march=native -fopenmp wave_2.5.c -lm ⏎
$ time ./a.out 0.0 15 15 1.2 1.0 90.0 30.0 -30. 40. -30. 30. 700 600 ⏎

real	0m0.926s
user	0m1.496s
sys	0m0.049s

「real」のところの値が端末で入力後にコマンドプロンプトが戻ってくるまでの時間になる。

表にまとめると

OpenMP  -O0   -O3  -O3+オプションもりもり
Off 4.9秒 3.7秒 1.2秒
On 1.5秒 1.3秒 0.9秒

0.9秒の「-O3 -ffast-math -march=native -fopenmp」が最速である。最適化無しに比べるとだいたい5倍速い。

ちなみに改定前のwave.c(バージョン1)のときは以下のとおり

なんとまあ,プログラムのブラッシュアップ(とGCCの差?)で最適化無しでの時間が27.3秒から4.9秒に短縮されてますよ!
Ubuntu 16.04LTS,gcc 5.4.0,Core i7-6820HQ

OpenMP -O0 -O3 -Ofast
Off 27.3秒 10.4秒 7.3秒
On 5.0秒 2.3秒 1.8秒

波の計算について

位置P(p,q) にある散乱体から発生する散乱波を考える。入射波はX方向に進む平面波

cos[2π(νt - x/λ)]

Pにおける入射波は

cos[2π(νt - p/λ)]

散乱波はπだけ位相が変わるので,Pで発生した散乱波はPにおいて

cos[2π(νt - p/λ - 1/2)]

位置(x, y)における散乱波はPとの距離をr = sqrt((x-p)*(x-p)+(y-q)*(y-q))として

cos[2π(νt - p/λ - 1/2 - r/λ)]

ただし,球面波なので散乱体から離れるにつれて振幅が減衰(1/sqrt(距離):エネルギーは1/(距離))するため

1/sqrt(r) * cos[2π(νt - p/λ - 1/2 - r/λ)]

となる。各散乱体に対してこの式を作成し,全てを足し合わせると散乱体の格子からの散乱波となる。

散乱体の行列

このルーチンが一番面倒だった。原点対称にするために,原点から(散乱体の個数/2)個並べていく形にした。 ソースプログラムを見てもらえば分かるだろう。
もっとエレガントなやり方を誰か教えてください。

何のことはない,並べてから散乱体の行列の中心を原点に平行移動すれば良いだけだった。
しかもこの方法なら散乱体の並びが直交してなくても簡単に作れるではないか!

シェルスクリプトが並列化されてないのでは?

気付いてしまったね。こちらへ