2. 同期問題を考える¶
早速ライフゲームを作成していきたいところですが、ルールを書き始める前にひとつ気を付けなければならない点があります。それが同期問題です。
artisoc Cloudは基本的に、ルールに書かれた処理を一つずつ順番に行っていきます。
すなわち、エージェントがたくさんいるとき、それぞれのエージェントは順にルールを実行していくということです。例えば、人エージェントが10人いて1ステップに1進むというルールの時には、1ステップの間にまず1人目が1進み、次に2人目が1進み…といった具合です。ただし、マップへの描画は1ステップごとに行われるので、1ステップに同時に10人が進んでいるように見えるわけです。
このため、今まではさほど気にする必要はなかったのですが、ライフゲームのような周りの状態をみて自分自身の状態を決めるモデルにおいては気を付ける必要があります。
2.1. 森林火災モデルで同期問題を理解する¶
同期問題を意識しないとどのようなことが起こるのか、実際にモデルを動かしてみるのが理解するための近道です。
簡単な森林火災のモデルをつくってみましょう。
森林火災のモデルは山に木が生えており、木が燃えるとその火が隣の木にどんどん燃え移っていくというモデルです。
新規モデルの作成を開始し、名前は森林火災としましょう。Universeの下にwoodsという空間をつくります。空間の設定では、種別は四角格子空間、空間の大きさは8×2で、ループはしないようにしてください。woodsの下にはtreeエージェントをつくります。そして、treeの下にはtreeが燃えているかどうかを表すエージェント変数colorを追加してください。
また、このモデルでは毎ステップ周りの木を見て自分の状態を決めるため、周りの木を格納する変数neighborsもtreeの下に追加しましょう。木は動かず周りのエージェントはいつも変わらないので、こうしておくと何度も周りのエージェントを探さずに済みます。
マップの出力も設定します。罫線はチェス型を選択してください。また、treeをマップ要素に追加するとき、色を変数指定(color)にしましょう。
ルールでは木が隙間なく並ぶようにします。最初のステップでは左下の木だけが燃えているようにします。燃えていない木は緑、燃えている木は赤で表現します。
def univ_init(self):
for x in range(8):
for y in range(2):
one=create_agt(Universe.woods.tree)
one.x = x
one.y = y
if x == y == 0:
one.color = COLOR_RED
else:
one.color = COLOR_GREEN
# すべての木について隣接する木の集合をneighborsに代入
for one in make_agtset():
one.neighbors = one.make_agtset_around_own_sqgrid(1, False, manhattan=True)
make_agtset_around_own_sqgrid関数は、四角格子空間において、あるエージェントの周囲にいるエージェントの集合を返す関数です。今回の例ではone.[関数名]
と記述することにより、 エージェントoneの周囲のエージェントを取得しています。1番目の引数は距離、2番目の引数は自分を含むかどうか(True or False)です。今回はさらに引数としてmanhattan=True
を追加することで、距離としてマンハッタン距離を指定しています(指定しない場合はチェビシェフ距離)。
エージェントのルールエディタ内ではself.[関数名]
の形で記述することにより、そのエージェント自身(self)の周囲のエージェントを取得することができます。
# 自分から距離5以内のエージェントを全て取得する(自分を含まない)
# (空間エージェントのルールエディタに記述)
neighbors = self.make_agtset_around_own(5, False)
次に、隣の木が燃えていれば燃え移り、燃え続ければ燃え尽きる(黒くなる)ルールを書いていきます。
ルール化すると以下のようになります。
def agt_step(self):
if count_step() > 1: # 燃え移るのは2ステップ目から
if self.color == COLOR_GREEN: # 自分が緑の時
for one in self.neighbors:
# 周りの木が1つでも赤ければ自分も赤くなる
if one.color == COLOR_RED:
self.color = COLOR_RED
break
else:
self.color = COLOR_BLACK
新しくbreak
が登場しました。break
を使用するとそれ以降の処理を行わずすぐにfor文から抜けることができます。
ここでは、周囲に何本燃えている木があったとしても、自分自身に燃え移ったら(赤くなったら)それ以上周りの燃えている木を探す必要がなくなるのでbreak
を使用しています。
これで準備は完了です。それでは、ステップ実行ボタンを1回ずつ押してください。変化がなくなったら停止ボタンを押し、一旦シミュレーションを終了してから再びステップ実行ボタンを1回ずつ押してみましょう。これを何度か繰り返すと様々な結果が見られたと思います。中には、最後まで火災の広がらないパターンもあります。
このように、実際のシミュレーション結果は「木が燃えるとその火が隣の木にどんどん燃え移っていく」という結果になっていません。これを同期問題と呼んでいます。
2.2. 同期問題はなぜ起こるか¶
なぜ火は最後まで燃え移らずに緑の木が残ることがあるのでしょうか?また、実行するたびに結果が異なるのはなぜでしょうか?
説明のために下図のように木に番号を付けます。
この時、最初に燃えている0番の木に隣接している木は1番と8番であるため、次に燃えているのはこの2本で、0番の木は燃え尽きて黒色になるはずです。
確かに、すべての木が「同時に」火が移ってくるかどうかを調べられたらそのような結果になるでしょう。しかし、実際にはこの章の冒頭で述べた通り、エージェントのルールを同時並行的に実行することはできません。artisoc Cloudでは実行するエージェントの順番を毎ステップ、ランダムに決めています。
もし、先に述べたような初期状態が与えられたとき、0番の木のルールが最初に実行されると0番の木は燃え尽きます。その次に1番の木のルールが実行されても既に燃えている木は1本もなく、火は全く燃え広がらなくなってしまいます。
一方で、1, 2, 3, 4, ……15, 0という順でエージェントのルールが実行された場合を考えてみましょう。この場合、常に隣の木が燃えている状態となり、1ステップの間にすべての木が燃えてしまいます。
このように、特に工夫を加えずルールを作成すると、実行する度に結果が変わってしまい再現したかった状態にはなりません。
では、私たちが直感的にイメージする意味合いでの「同時」にエージェントの状態を変えるにはどうしたらよいのでしょうか。
シミュレーションにおける「同時」(1ステップのうちに行われている様々なこと)を直感的な「同時」に合わせることを「同期」させると言います。同期させる方法を学んでいきましょう。
2.3. 同期の方法¶
artisoc Cloudでは同期させる方法が主に2種類あります。
一つは状態の変化を直ちに実行せず、各ステップの最後にまとめて行う方法です。もう一方は、周囲のエージェントの現在の状態ではなく、1ステップ前の状態に応じて状態を変化させる方法です。
それでは一つ目の、状態の変化を直ちに実行せず、各ステップの最後にまとめて行う方法について学習していきます。
まず、現在の木の状態を表す変数colorのほかに、次の瞬間にどの状態になるのかを示す別のエージェント変数next_colorをモデルツリーに追加しましょう。そして、エージェントルールでは状態が変わるとき、すぐにcolorを変えるのではなく、next_colorを変えておきます。
まず、next_colorの初期化を行いましょう。Universeルールが次のようになります。
(ちなみに、赤の次は黒になるはずですが、最初だけ赤い木のnext_colorを赤にしているのは、いきなり黒くならないようにするためです。)
def univ_init(self):
for x in range(8):
for y in range(2):
one=create_agt(Universe.woods.tree)
one.x = x
one.y = y
if x == y == 0:
one.color = COLOR_RED
one.next_color = COLOR_RED # ここを追加
else:
one.color = COLOR_GREEN
one.next_color = COLOR_GREEN # ここを追加
for one in make_agtset():
one.neighbors = one.make_agtset_around_own_sqgrid(1, False, manhattan=True)
エージェントルールは次のように書き換えましょう。
def agt_step(self):
if count_step() > 1:
if self.color == COLOR_GREEN:
# 現在緑だったら基本的には次も緑
self.next_color = COLOR_GREEN
for one in self.neighbors:
# もし周りの木が1つでも赤ければ自分のnext_colorを赤に変更
if one.color == COLOR_RED:
self.next_color = COLOR_RED
break
else:
# 現在緑以外だったら(赤か黒だったら)次は黒
self.next_color=COLOR_BLACK
そして、全エージェントのnext_colorが置き換えられたあと、Universeのルールで一括してcolorを置き換えます。
def univ_step_end(self):
for one in make_agtset():
one.color = one.next_color
ここでは再びmake_agtset関数ですべてのエージェントを含むエージェント集合を作成しています。毎ステップ同じエージェント集合を作ることになるので、Universe下にall_treeなどの名前でエージェント集合を格納するための変数を用意してもよいでしょう。余力がある人は変数all_treeを作り、univ_initであらかじめall_treeにエージェント集合を格納し、使いまわすようにルールを変えてみてください。
それでは保存して実行してみましょう。
今度は期待した結果になったでしょうか。何度実行しても木が端から端まで順番に燃え尽きれば、ルールは正しく書けていることでしょう。
このように、同期をする一つの方法としては、エージェントに次の瞬間に変わる状態を保存しておく変数を用意し、すべてのエージェントが次の瞬間の状態を変数に保存した後に一括して現在の状態の変数に代入します。
2.4. 同期の方法2¶
次に、周囲のエージェントの現在の状態ではなく、1ステップ前の状態に応じて状態を変化させる方法についても見ていきましょう。
森林火災のモデルに当てはめて具体的に説明すると、自分自身が1ステップ前に燃えていれば今燃え尽きるし、周囲の木が1ステップ前に燃えていれば自分は今燃える、といったように、1ステップ前の状態を参照して行動を決めます。
このように1ステップ前の状態を参照するためにはget_history関数を使用する必要があります。get_history関数を使えば指定した過去のステップにおけるエージェントを取得することができます。
# 1ステップ前のtaro(エージェント)を取得
get_history(taro, 1)
この方法を使って森林火災モデルを書き換えてみましょう。
今回は先ほど追加したnext_colorは不要です。また、univ_step_endのルールは消しておいてください。ルールを消した際には、「何もしない」という意味のpassを忘れずに書いておいてください。
まず、ルールの編集画面でエージェントtreeのプロパティを開いてください。記憶数の欄に1と入力します。
この記憶数の欄で、何ステップ分の情報を保存しておくかを指定することができます。記憶数は多ければ多いほどシミュレーションが重たくなってしまうため、必要最小限の数字を指定するようにしましょう。今回は、エージェントtreeの1ステップ前を参照できれば良いので1を指定しました。
これでget_history関数を使うための準備が整いました。
続いてルールの変更をします。エージェントルールを以下のように書き換えましょう。
def agt_step(self):
if count_step() > 1:
past_tree = get_history(self, 1)
if past_tree.color == COLOR_GREEN:
for one in self.neighbors:
past_neighbor = get_history(one,1)
if past_neighbor.color == COLOR_RED:
self.color = COLOR_RED
break
else:
self.color=COLOR_BLACK
では保存して実行してください。
こちらの方法でも何度実行しても木が端から端まで順番に燃え尽きる様子が再現できたでしょうか?
1ステップ目はそれ以前に参照する過去がないことにも留意しましょう。今回はif count_step() > 1:
としているので2ステップ目からget_history関数を使ったルールが実行されうまくいっています。
これらの2つの方法はどちらも使うことができますが、作りたいモデルや状況によって使い分けましょう。
1つ目の変数next_colorを使った方法はマップ出力と関係のない抽象的な変数を増やす必要があるのでやや煩雑になってしまいます。
一方、2つ目のget_history関数を使った方法は比較的シンプルな方法です。しかし、get_history関数を使うためにはすべてのエージェントの持つすべての変数を保存しておかなければならないので、モデルが重くなってしまいます。
このような特徴を踏まえて、使い分けの一例としてはエージェント数が少ないモデルではget_historyを使用し、モデルの実行速度が遅くなってしまうときやエージェント数が多いときは1つ目の方法を使用するのが良いでしょう。
今回作成するライフゲームはエージェント数が多いほどおもしろいモデルになるので、1つ目に紹介した状態の変化を直ちに実行せず、各ステップの最後にまとめて行う方法を用いて説明します。
さて、周囲のエージェントの状態によって自身の状態を決定するモデルでは、同期問題に気を付けなければならないことを理解できたでしょうか。
ここまでくればライフゲームのモデルはすぐに作成することができます。次章ではいよいよライフゲームを作成します。
2.5. 参考¶
この章のサンプルモデルは次の通りです。