MolecularDynamics
LAMMPS

LAMMPS入門の手引き 4:LAMMPS内部のオブジェクトあれこれ

今回は例題はなくて解説がメインになります。

LAMMPSでは、コマンドを通して様々な設定をしますが、大きく分けると以下の2種類があります。


  • 系にはじめから存在しており、コマンドを通して選んだり値を設定したりするもの (pair_styleとかtimestepとか)

  • 系に存在しておらず、コマンドを通して新しく作ったり消したりするもの(variableとかfixとか)

後者は基本的に新しいものを定義するたびにID(名前)をふって管理します。LAMMPSでは実行時にこれら(この記事ではオブジェクトと呼びます)が呼ばれたり参照されたりしながら計算が進んでいきます。オブジェクトはいくつかの系統にわかれているのですが、一部は似ている概念のものもありややこしいと思います。今回の記事はオブジェクトの整理を目的にしています。


region, group

regionは空間を定義するものです。create_boxコマンドで使うので1回は使うことになるでしょう。

単純な直方体に限らず様々な形状が用意されています。groupを定義する際などに補助的に使うと便利でしょう。

groupは原子の集合を定義するものです。特定の原子をずっと追いかけることができます。

定義の仕方には様々な方法があり、regionから、原子のIDから、variableから(後述)など様々です。またgroup同士の和集合や差集合なども定義できます。「この指定した空間以外のすべての原子」みたいなgroupをとるときに便利です。

また、はじめから「すべての原子」を指すgroupとしてallが定義されています。fixコマンドなどで2つ目に指定しているのがこれです。当然fixcomputeなどではall以外のgroupをとれます。あるエリア内だけの温度が欲しいとか、ある原子だけ平均二乗変位が欲しいとかの使いみちが考えられます。

困ることはあまりないと思いますが、原子ごとにビットを立ててgroupを管理している実装の都合上、groupの数は最大32個です。


variable

variableというコマンド名からプログラミング言語における変数の定義のように感じられますが、これは間違いで、実際にはvariableはどこかで参照されるたびに内部の値が計算されます。例えばvariableに温度を代入(?)しておくと、参照するたびにその瞬間の温度が返ってきます。

例えば

variable t1 equal temp

と書けばt1は系の温度(temp)を参照します。変数への代入を念頭に考えると、t1にはこのコマンドを書いた時点での温度の値が保存されると思ってしまいますが、この理解は間違いです。実際にはt1は他から参照されるたびにその都度温度を計算して値を返します。

このtempというのはthermo_styleで指定したものです。右辺には定数、他のvariableの値、fixやcomputeの値、thermo_styleで指定した物性値、その他いくつかの数学関数など様々なものを使うことができ、それらの四則演算や関数適用などを書くことができます。

variableと後述するfix, computeが出力する値には大きくわけて3x3種類あります。一般にこれらを混合することはできません。マニュアルでは以下のように呼ばれています。

global variable
per-atom variable
local variable

Scalar
global scalar
per-atom scalar
local scalar

Vector
global vector
per-atom vector
local vector

Array
global array
per-atom array
local array

scalar, vector, arrayは値がスカラーか1次元配列か2次元配列かの違いです。扱いはほとんど変わりません。配列の場合はインデックスを例えばv1[1]のように指定すると次元が落ちてスカラー扱いになります。

global variableは系全体でひとつの値を持つものです。上の例はすべてこちらです。一方、per-atom variableは原子ごとに値を持つものです。原子の数だけ値があることを考えるとper-atom scalarと global vectorは似たものにも見えますが、LAMMPSでの扱いは明確に異なっています。LAMMPSはマルチプロセスで空間領域を分割して計算を進めているので、全ての原子が見えているプロセスというのは存在していません。プロセスごとに自分の領域の原子だけを管理しており、per-atom variableも同様の扱いとなっています。原子ごとにおまけの属性をつけてあげられるもの、という考え方もできます。

local variableはfixとcomputeにしかない概念です。プロセスごとに自分の領域の中だけの値を持つもので、per-atomと似ていますが、原子ひとつずつに紐付かない値(原子間の結合ごとの値とか)を持つものに対して使われます。

per-atom variableを使えば、例えば以下のような使い方もできます。

variable a1 atom (y-10*sin(x))<0

group g1 variable a1

こうすると全原子のうちy=10*sin(x)の曲面より下の原子だけを含むgroupを作ることができます。per-atom variableを出力するfixやcomputeもあるため、組み合わせるともっと複雑なこともできます。

per-atom variableをglobal variableが要求されているところに使うことはできません。一応、compute reduceコマンドでper-atom variableの和をとったり最大値をとったりしたglobal variableをつくることは可能です。

また、原子にはユニークなIDがふってありますが、デフォルトではIDから原子を逆引きすることはできません。これを可能にするにははじめに以下のように逆引きを認めるよう指定しておく必要があります。(効率の点からデフォルトではオフになっています。)

atom_modify id yes

これを指定しておけば、per-atom variableについて原子のIDをインデックスとして使うことも可能になります。

variableは他のコマンドでvariableが要求されているところに書くことができますが、variableにはもう一つ、他のコマンドにはない独特の使い方があります。スクリプト中の任意の位置で${t1}のように使うと、その時点でのvariableが指す値を計算し、その値がそこに直書きされているかのように振る舞わせることができます。

少々トリッキーですが、これを利用すると、

variable t1 equal temp

variable t2 equal ${t1}

と書くことでt1に現在の温度(時間とともに変化)、t2に定義した瞬間の温度(定数)を代入することができます。これは実行中の値をとってきた例ですが、単にスクリプトの一部をvariableで置き換えておくというだけでも設定温度や出力ファイル名など様々なものをvariable化しておいてスクリプトの抽象度を上げることができます。抽象度を上げておけば再利用性が高まります。


fix

fixはregionやvariableのような定義するだけでは結果に何も影響しないオブジェクトではなく、一般に系に影響を与えるオブジェクトです。

具体的には、runコマンドでMDを実行している最中は、1ステップごとにすべての定義したfixが呼ばれて各自の仕事を行います。どちらかというとこの「毎ステップ対応する処理が実行される」というのがfixの特徴です。

fixが作用する範囲は、ひらたく言ってしまえば「ほぼ何でも」です。原子の位置を動かす、原子に力を与えるといった想像しやすいものから、格子の大きさを変える、原子を追加するといった大規模な変更を伴うもの、系には何も影響を与えずvariableを一定ステップおきに平均するだけといったものまで様々です。

最後の例のように、一部のfixは値を出力します。これはvariableでfixのidを通して参照することが可能です。この点ではvariableとfixとcomputeはよく似ています。

実は、LAMMPSのrunコマンドの中身は、ポテンシャル関数に基づく力の計算とプロセス間のやりとりを行っている以外は順にfixを呼び出しているだけだったりします。MDの原理を習うと出てくる速度ベルレ法による原子の更新はrunではなくfix nveの中に実装されています。


compute

variableでは書ききれないような様々な値を計算してくれるものです。動径分布関数、平均二乗変位などMDを行ううえでしばしば必要になる様々な量をコマンドひとつで計算してくれるため便利です。

実は、thermo_styleで温度などを計算する際はLAMMPSが裏で対応するcomputeを定義しています。

fixとの顕著な違いとして、computeは他から呼ばれて計算が必要にならない限り計算されません。なので、computeを定義したら即座にLAMMPSの実行速度が遅くなるということはありません。

variableからfixやcomputeを使う際に直面するややこしい話題があるので、ここで紹介しておきます。variableを使う場所としては、大きく分けて「runコマンドによるMDの実行中(thermoやfixの中など)」と「MDの実行前あるいは後(printやwrite_dump、$形式など)」の2パターンがあります。このうち問題になるのは後者の場合です。

variableの章で「variableはどこかで参照されるたびに内部の値が計算されます」と書きました。これは正しいのですが、variableが参照する先の別の値もそうであるとは限りません。具体的に言うと、fixcomputeなどはMD実行中に計算されますが、MD実行後には値が更新されることはありません。つまり、runコマンドやminimizeコマンドを実行しない限りfixcomputeなどは一度も計算されません。(thermo_styleで定義されたものも同様です。)これらの値をvariableで使うと問題が発生します。

具体的に問題が起きるスクリプトを以下に示します。

pair_coeff * * 1.0 1.0

run 1000
pair_coeff * * 2.0 2.0
variable energy1 equal pe
print "Potential energy = ${energy1}"

こういうことをすると、最後のprintコマンドでは3行目でポテンシャルを変更した以前のエネルギーが表示されてしまいます。

ではどうすればいいかというと、run 0を使います。run 0は文字通り解釈すると0ステップMDを実行するという何の意味もないコマンドですが、実際にはMD開始前と終了後の処理を行ってくれるため、系を変更することなく内部で持っている様々な値を更新するという振る舞いをします。そのため、LAMMPSで原子の位置やポテンシャルなど何かを直接変更した直後に物性値などをとりたい場合は先にrun 0を書くようにしましょう。

これは挙動の隙を突いたハックというわけではなく、公式の解決法です。Variableの説明の"Variable Accuracy"のところを見ると詳しく解説されています。


dump

dumpコマンドは系の構造をファイルに出力するときに使います。出力形式はいろいろありますが、基本的には原子の情報を出力します。per-atom variableを出力させることもできます。指定したステップ数ごとに呼び出されます。


ところで、LAMMPSの実装では、今回オブジェクトと呼んでいるものたちはC++の抽象クラスを使って定義されています。(LAMMPSの本体がfix, computeなどいくつかの抽象クラスの配列を持っています。)それぞれの具体的なコマンドは抽象クラスを継承する形で書かれています。

このため、ソースファイルの多くはLAMMPSのコマンド単位で1ファイル1クラスの形で管理されています。

LAMMPS入門の手引き 3:データの保存と制御文