今回の目標
前回の「人工生命クランの開発 その2」では、移動して、食事して、増殖して、死んでいく人工生命らしきものを作成し、blittingを使って効率的に描画できるようコードを修正しました。
Q.E.D.第8話「ヤコブの階段」に記載されている人工生命のシミュレーションの設定は、
- 自分の能力を把握しなさい
- 自分より弱いものを従え、強いものに従いなさい
- 繁殖に適した場所を探しなさい
- 絶滅を避けるために繁殖しなさい
- セルの寿命は3日
- 世代交代すると変異する
です。今回は、セルの寿命を設定しつつ、変異を実装したいと思います。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.animation import PillowWriter
WIDTH = 30
HEIGHT = 30
CYCLES = 300
DENSITY = 0.01
fig, (ax, ax_graph) = plt.subplots(1, 2, figsize=(12,6))
class Cell:
def __init__(self):
self.active = False
self.x = 0
self.y = 0
self.col = 'white'
self.life = 0
self.speed = 0
self.threshold = 0
self.circle = plt.Circle((self.x, self.y), radius=0, facecolor=self.col)
self.circle.set_animated(True)
self.circle.set_zorder(10)
ax.add_patch(self.circle)
def activate(self, x, y, col='white', life=3, speed=1, threshold=8):
self.active = True
self.x = x
self.y = y
self.col = col
self.life = life
self.speed = speed
self.threshold = threshold
self.circle.center = (self.x, self.y)
self.circle.set_radius(0.1 * self.life)
self.circle.set_facecolor(self.col)
self.circle.set_visible(True)
def deactivate(self):
self.active = False
self.circle.set_visible(False)
def update(self):
if self.active == False:
return self.circle
else:
self.life -= 1
if self.life <= 0:
self.deactivate()
else:
self.move()
self.eat()
self.divide()
self.circle.center = (self.x, self.y)
self.circle.set_radius(0.1 * self.life)
return self.circle
def move(self):
self.x += np.random.randint(-self.speed, self.speed + 1)
self.y += np.random.randint(-self.speed, self.speed + 1)
self.x = int(np.clip(self.x, 0, WIDTH))
self.y = int(np.clip(self.y, 0, HEIGHT))
def eat(self):
index = self.y * (WIDTH + 1) + self.x
field_obj = fieldList[index]
if field_obj.life == 1:
field_obj.life -= 5
self.life += field_obj.energy
def divide(self):
if self.life >= self.threshold:
self.life //= 2
for cell in cellPool:
if not cell.active:
cell.activate(x=self.x, y=self.y)
break
class Field:
def __init__(self, x, y, size=1, energy=2, col='green'):
self.x = x
self.y = y
self.size = size
self.life = 1
self.energy = energy
self.facecolor = col
self.rectangle = plt.Rectangle((self.x-self.size/2, self.y-self.size/2), self.size, self.size, facecolor=self.facecolor)
self.rectangle.set_animated(True)
self.rectangle.set_zorder(1)
ax.add_patch(self.rectangle)
def update(self):
if self.life < 1:
self.rectangle.set_facecolor('brown')
self.life += 1
else:
self.rectangle.set_facecolor(self.facecolor)
return self.rectangle
fieldList = []
for i in range(HEIGHT+1):
for j in range(WIDTH+1):
fieldList.append(Field(j, i))
poolSize = len(fieldList)
cellPool = []
for _ in range(poolSize):
cellPool.append(Cell())
num_active = int(poolSize * DENSITY)
for i in range(num_active):
x = np.random.randint(WIDTH)
y = np.random.randint(HEIGHT)
cellPool[i].activate(x, y)
class LineGraph:
def __init__(self, cellPool):
self.cellPool = cellPool
self.activeCellCount = [sum(cell.active for cell in self.cellPool)]
x_data = np.arange(len(self.activeCellCount))
self.line, = ax_graph.plot(x_data, self.activeCellCount, animated=True)
def update(self):
self.activeCellCount.append(sum(cell.active for cell in self.cellPool))
x_data = np.arange(len(self.activeCellCount))
self.line.set_data(x_data, self.activeCellCount)
return self.line
lineGraph = LineGraph(cellPool)
def init():
ax.set_xlim([-1, WIDTH+1])
ax.set_ylim([-1, HEIGHT+1])
ax.set_xticks([])
ax.set_yticks([])
ax.set_aspect('equal')
ax.set_title("Cycle 0")
ax.title.set_animated(True)
ax_graph.set_xlim([0, CYCLES])
ax_graph.set_ylim([0, 200])
ax_graph.set_xlabel('Cycle')
ax_graph.set_ylabel('Number of Cell')
artists = [ax.title] + [field.rectangle for field in fieldList] + [cell.circle for cell in cellPool] + [lineGraph.line]
return artists
def animate(i):
ax.set_title(f"Cycle {i}")
if i > 0:
for cell in cellPool:
cell.update()
for field in fieldList:
field.update()
lineGraph.update()
artists = [ax.title] + [field.rectangle for field in fieldList] + [cell.circle for cell in cellPool] + [lineGraph.line]
return artists
ani = animation.FuncAnimation(fig, animate, frames=CYCLES, init_func=init, blit=True)
writer = PillowWriter(fps=2)
ani.save("animation.gif", writer=writer)
plt.close(fig)
寿命の設定
これは簡単です。セルの属性にageを追加して、更新する毎に1増えていくようにして、寿命を越えたら死亡します。
# 定数に寿命を設定
LIFE_SPAN = 30
class Cell:
def __init__(self):
self.age = 0
def activate(self, x, y, col=[0, 0, 0], life=3, speed=1, threshold=8):
self.age = 0
def update(self):
if self.active == False:
return self.circle
else:
self.life -= 1
if self.life <= 0:
self.deactivate()
# 更新する毎に年齢を重ねる
self.age += 1
# 寿命が尽きたら死ぬ
if self.age > LIFE_SPAN:
self.deactivate()
# 体力があって寿命が尽きていなければ移動、食事、分裂
else:
self.move()
self.eat()
self.divide()
self.circle.center = (self.x, self.y)
self.circle.set_radius(0.1 * self.life)
return self.circle
変異を実装する
分裂時、基本的には親セルの能力を引き継ぐのですが、一定の割合で初期体力、移動速度、分裂閾値などの属性が変わるようにします。また、それぞれの数字が変わったときに、セルの色が変わって、どういう能力を獲得したのかわかるようにします。
色をRGBで表現すると、ノーマルセルを黒色(0,0,0)として、初期体力が3のところが4に増えると赤色(1,0,0)、移動速度が1から2に増えると緑色(0,1,0)、分裂閾値が8から6に変わると青色(0,0,1)となるようにします。こうすれば、変異が二つ、三つと重なることも表現できます。例えば、初期体力と移動速度の変異が重なると(1,1,0)で黄色に、全て変異すると(1,1,1)で白色のセルになります。ただし、タプルは変更できないのでリスト形式で使用します。
また、lifeの初期値を引き継げるように、lifeという属性だけではなくてinitialLifeという属性を作る必要があります。
class Cell:
def __init__(self):
# 分裂時に引き継げるようにinitialLifeという属性を新たに作成する
self.initialLife = 0
def activate(self, x, y, col=[0, 0, 0], speed=1, threshold=8, initialLife=3):
self.life = initialLife # 生まれた時の体力はinitialLife
self.initialLife = initialLife
def divide(self):
if self.life >= self.threshold:
self.life //= 2
for cell in cellPool:
if not cell.active:
# 初期値は親から属性を引き継ぐ
new_speed = self.speed
new_threshold = self.threshold
new_initialLife = self.initialLife
new_col = self.col.copy()
# 一定の割合で突然変異する。能力を失うような逆方向の変異もありうる。
if np.random.rand() < MUTATION_RATE:
if self.col[0] == 0:
new_col[0] = 1 # 赤色のセルは初期体力が多い
new_initialLife += 1
elif self.col[0] == 1:
new_col[0] = 0
new_initialLife -= 1
elif np.random.rand() < MUTATION_RATE * 2:
if self.col[1] == 0:
new_col[1] = 1 # 緑色のセルは移動が早い
new_speed += 1
elif self.col[1] == 1:
new_col[1] = 0
new_speed -= 1
elif np.random.rand() < MUTATION_RATE * 3:
if self.col[2] == 0:
new_col[2] = 1 # 青のセルは分裂の閾値が減る
new_threshold -= 2
elif self.col[2] == 1:
new_col[2] = 0
new_threshold += 2
# 非活性なセルを新たな分裂後のセルとして活性化
cell.activate(x=self.x, y=self.y, col=new_col, speed=new_speed, threshold=new_threshold, initialLife=new_initialLife)
break # 1 つだけ活性化すればループを抜ける
セルの種類毎に集計して折れ線グラフにする
count_historyという辞書のリストを作成します。あるサイクルで、各色のセルが何個あるかを辞書形式で記録し、それをサイクル順にまとめたリストです。これを利用して折れ線グラフを作成します。なお、辞書のkeyはセルの色ですが、辞書のkeyとして使うときにはタプルに変える必要があります。
また、前回作成したLineGraphクラスとは少し変えて、cell_count関数を作っています。init()とupdate()で同じようなコードが並んでいたので、まとめてスッキリしました。
# シミュレーションの各サイクルでセルのカウントを更新し、時間変化としてグラフ化する
class LineGraph:
# セルオブジェクトのリストを受け取る。各セルが持つactive属性や色を使ってカウントする
def __init__(self, cellPool):
self.cellPool = cellPool
# サイクル毎に、セルの色ごとの個数を記録した辞書のリスト
self.count_history = []
# 各色に対応する折れ線グラフ(Line2Dオブジェクト)を保持する辞書。
self.line_objects = {}
# 凡例用のラベル辞書。長いとグラフ表示を邪魔するので略語にする。
legend_label_dict = {
(0, 0, 0): 'N', # Normal
(0, 0, 1): 'T', # Low threshold
(0, 1, 0): 'M', # Fast moving
(0, 1, 1): 'MT',
(1, 0, 0): 'L', # High Life
(1, 0, 1): 'LT',
(1, 1, 0): 'LM',
(1, 1, 1): 'LMT'
}
# cell_count()を使って、初期状態でのアクティブなセルの色ごとの個数を取得
initial_count = self.cell_count()
# 得られた initial_count辞書をself.count_historyに追加
self.count_history.append(initial_count)
# initial_countの各色とそのカウントを走査し、グラフ軸(ax_graph)上に描画
for col_key, count in initial_count.items():
label = legend_label_dict[col_key] # ラベルを取得
line, = ax_graph.plot([0], [count], color=col_key, animated=True, label=label)
self.line_objects[col_key] = line # 返されたLine2Dオブジェクトをself.line_objectsに保存
# 凡例を表示する
ax_graph.legend()
def cell_count(self):
# 現在のセルの状態から色ごとの個数をカウント
# カウント用辞書の初期化
count_dict = {(0, 0, 0): 0,
(0, 0, 1): 0,
(0, 1, 0): 0,
(0, 1, 1): 0,
(1, 0, 0): 0,
(1, 0, 1): 0,
(1, 1, 0): 0,
(1, 1, 1): 0}
# for cell in self.cellPool: で全セルをチェック
# もし cell.active が True であるなら、セルの colをタプルに変換してキーとし、そのカウントを1つ増やす。
for cell in self.cellPool:
if cell.active:
count_dict[tuple(cell.col)] += 1
return count_dict
def update(self):
current_count = self.cell_count() # 最新サイクルでの各色ごとのセル数を取得
self.count_history.append(current_count) # その結果をself.count_historyに追加、すべてのサイクルの履歴を保持
for col_key, count in self.count_history[-1].items(): # 最新サイクルの各キーとそのカウントを取得
line = self.line_objects[col_key] # 取得したキーに対応する折れ線グラフオブジェクト
x_data = np.arange(len(self.count_history)) # サイクル数に応じた x軸のデータを作成
# 各サイクルごとのその色のセル数をリスト内包表記でまとめる
y_data = [history.get(col_key, 0) for history in self.count_history]
line.set_data(x_data, y_data) # 各ライングラフのデータ点を新しい履歴に合わせて更新
return list(self.line_objects.values())
# cellPoolを渡してLineGraphのインスタンスを生成
# これ以降は、各更新サイクルで lineGraph.update()を呼び出す
lineGraph = LineGraph(cellPool)
# 初期化関数
def init():
ax.set_xlim([-1, WIDTH+1])
ax.set_ylim([-1, HEIGHT+1])
ax.set_xticks([])
ax.set_yticks([])
ax.set_aspect('equal')
ax.set_title("Cycle 0")
ax.title.set_animated(True)
# グラフ用の軸の初期設定
ax_graph.set_xlim([0, CYCLES])
ax_graph.set_ylim([0, 200])
ax_graph.set_xlabel('Cycle')
ax_graph.set_ylabel('Number of Cell')
ax_graph.set_facecolor('lightgray') #白色のセル用折れ線グラフのため、背景を灰色にする
artists = ([ax.title] +
[field.rectangle for field in fieldList] +
[cell.circle for cell in cellPool] +
list(lineGraph.line_objects.values()))
return artists
def animate(i):
ax.set_title(f"Cycle {i}")
if i > 0:
for cell in cellPool:
cell.update()
for field in fieldList:
field.update()
lineGraph.update()
artists = ([ax.title] +
[field.rectangle for field in fieldList] +
[cell.circle for cell in cellPool] +
list(lineGraph.line_objects.values()))
return artists
何度か試してみたところ、最終的には白色で埋め尽くされるのですが、途中までは緑色が優勢になることが多いとわかりました。移動速度が有利なようです。
また、だいたい200サイクル程度で150-200個のセル数となり安定するようです。
が、ランダムなので回数をこなさないと正確なことはわかりません。
というわけで、次回はセルの挙動を描画しないで何度もシミュレーションを繰り返し、結果のみ吐き出すようなコードを書いてみます。
ただし、目標は人工生命クランの作成であり、リーダー能力や繁殖に適した場所を目指す本能等についても作っていきたいです。

