Processing でグラフを描く㉒ 鳥と餌のシミュレーション
Processing でグラフを描く 第22回目です。
人工生命シリーズ第3回目になります。第1回は「ライフゲーム」で、周囲のセルの状態により生死を繰り返す原始生物を作成しました。第2回は鳥の群れの行動を再現する「ボイドモデル」を作成しました。今回は「鳥と餌のシミュレーション」で人口と食料について考えていきます。
鳥と餌のシミュレーションでは、次のルールを設定しました。
鳥は閉鎖空間に閉じ込められており、その中で生存、繁殖、死亡を行う。
閉鎖空間は碁盤上に分割されており、餌が条件により自然に生えてくる。
鳥は自由に移動でき、移動先に餌があると食べることでHP(体力)を増やすことができる。
鳥は、寿命またはHPがなくなると死亡する。HP が一定値以上のとき、自分と同じ色の子供を生む(繁殖する)ことができる。
以上の条件で鳥の個体数を記録していきます。鳥の個体数の変化をグラフで表し考察していきます。
鳥を表示するプロブラム
import random
# スクリーンの設定
SCREEN_SIZE = 1000
PATCH_SIZE = 20
# 鳥の設定
bird_list = [] # 鳥を入れておくための空のリスト
BIRD_COLORS = [
'#FFA5CC', # pink
'#FFC726', # yellow
'#FF3424', # red
'#80FF25', # green
'#A0D4FF', # skyblue
'#CCCCCC', # white
'#FEA89F', # lightpink
'#2A2B4F', # darkblue
]
bird_num = 400
lifespan_over_num = 0
hp_over_num = 0
born_num = 0
def setup():
size(SCREEN_SIZE, SCREEN_SIZE)
frameRate(10)
for i in range(bird_num):
bird_list.append(Bird())
def draw():
background(0)
# 鳥の群れを表示
for bird in bird_list:
bird.update()
bird.draw()
# テキストを表示
fill(255)
text('life cycle: ' + str(frameCount), 10, 15)
text('bird num: ' + str(len(bird_list)), 10, 30)
text('lifespan over: ' + str(lifespan_over_num), 10, 45)
text('health point over: ' + str(hp_over_num), 10, 60)
text('born: ' + str(born_num), 10, 75)
class Bird:
def __init__(self, type_id=None):
"""鳥の初期化"""
self.position = [random.uniform(0, width), random.uniform(0, height)]
self.type_id = type_id if type_id else random.randint(0, 7)
self.speed = PATCH_SIZE
self.col = BIRD_COLORS[self.type_id]
self.direction = [0, 0]
self.velocity = [0, 0]
self.health_point = 500
self.radius = self.health_point / 100.0
self.lifespan = random.randint(200, 500)
def update(self):
global lifespan_over_num, hp_over_num, born_num
self.lifespan -= 1
if self.lifespan > 0:
self.direction = random.uniform(0, TWO_PI)
self.velocity = scale_vector(self.speed, [cos(self.direction), sin(self.direction)])
self.health_point -= self.speed
self.position[0] += self.velocity[0]
self.position[1] += self.velocity[1]
# 鳥が壁にぶつかったら反対側に通り抜ける
if self.position[0] > width:
self.position[0] = self.position[0] - width
if self.position[0] < 0:
self.position[0] = self.position[0] + width
if self.position[1] > height:
self.position[1] = self.position[1] - height
if self.position[1] < 0:
self.position[1] = self.position[1] + height
# 鳥の体力がなくなると死亡する
if self.health_point <= 0:
print('health point over')
hp_over_num += 1
bird_list.remove(self)
else:
self.radius = self.health_point / 100.0
else:
# 寿命がなくなると死亡する
print('lifespan over')
lifespan_over_num += 1
bird_list.remove(self)
def draw(self):
pushMatrix()
translate(self.position[0], self.position[1])
rotate(self.direction)
stroke(255)
fill(self.col)
ellipse(0, self.radius, 5, 5)
ellipse(0, -self.radius, 5, 5)
ellipse(0, 0, self.radius * 2, self.radius * 2)
fill(0)
ellipse(self.radius * cos(PI / 6), self.radius * sin(PI / 6), 3, 3)
ellipse(self.radius * cos(-PI / 6), self.radius * sin(-PI / 6), 3, 3)
popMatrix()
# ベクトル操作の関数
def scale_vector(scalar, v):
return list(scalar * coord for coord in v)
400匹の鳥を $${1000 \times 1000}$$ の空間にランダムに配置して、自由に移動させます。鳥は移動すると体力が減少し、体力が 0 になると死亡します。餌が与えられないため、鳥たちはすぐに全滅してしまいました。
次に餌を配置して、鳥の生存期間を延ばします。
餌を表示するプログラム
import random
# スクリーンの設定
SCREEN_SIZE = 1000
PATCH_SIZE = 20
# 鳥の設定
bird_list = [] # 鳥を入れておくための空のリスト
BIRD_COLORS = [
'#FFA5CC', # pink
'#FFC726', # yellow
'#FF3424', # red
'#80FF25', # green
'#A0D4FF', # skyblue
'#CCCCCC', # white
'#FEA89F', # lightpink
'#2A2B4F', # darkblue
]
bird_num = 400
lifespan_over_num = 0
hp_over_num = 0
born_num = 0
# 餌の設定
feed_list = []
FEED_COLORS = [
'#006336', # Vert Cypres
'#986B35' # kuwatya
]
feed_grow_rate = 0.1
feed_power = 40
def setup():
size(SCREEN_SIZE, SCREEN_SIZE)
frameRate(10)
for i in range(bird_num):
bird_list.append(Bird())
for i in range(int(SCREEN_SIZE / PATCH_SIZE)):
feed_list.append([])
for j in range(int(SCREEN_SIZE / PATCH_SIZE)):
feed_list[i].append(Feed(i, j))
def draw():
background(0)
# 餌を表示
for i in range(int(SCREEN_SIZE / PATCH_SIZE)):
for j in range(int(SCREEN_SIZE / PATCH_SIZE)):
feed_list[i][j].update()
feed_list[i][j].draw()
# 鳥の群れを表示
for bird in bird_list:
bird.update()
bird.draw()
# テキストを表示
fill(255)
text('life cycle: ' + str(frameCount), 10, 15)
text('bird num: ' + str(len(bird_list)), 10, 30)
text('lifespan over: ' + str(lifespan_over_num), 10, 45)
text('health point over: ' + str(hp_over_num), 10, 60)
text('born: ' + str(born_num), 10, 75)
class Bird:
def __init__(self, type_id=None):
"""鳥の初期化"""
self.position = [random.uniform(0, width), random.uniform(0, height)]
self.type_id = type_id if type_id else random.randint(0, 7)
self.speed = PATCH_SIZE
self.col = BIRD_COLORS[self.type_id]
self.direction = [0, 0]
self.velocity = [0, 0]
self.health_point = 500
self.radius = self.health_point / 100.0
self.lifespan = random.randint(200, 500)
def update(self):
global lifespan_over_num, hp_over_num, born_num
self.lifespan -= 1
if self.lifespan > 0:
self.direction = random.uniform(0, TWO_PI)
self.velocity = scale_vector(self.speed, [cos(self.direction), sin(self.direction)])
self.health_point -= self.speed
self.position[0] += self.velocity[0]
self.position[1] += self.velocity[1]
# 鳥が壁にぶつかったら反対側に通り抜ける
if self.position[0] > width:
self.position[0] = self.position[0] - width
if self.position[0] < 0:
self.position[0] = self.position[0] + width
if self.position[1] > height:
self.position[1] = self.position[1] - height
if self.position[1] < 0:
self.position[1] = self.position[1] + height
x = int(self.position[0] / PATCH_SIZE)
y = int(self.position[1] / PATCH_SIZE)
if not feed_list[x][y].eaten:
self.health_point += feed_list[x][y].feed_power
feed_list[x][y].eaten = True
# 鳥の体力がなくなると死亡する
if self.health_point <= 0:
print('health point over')
hp_over_num += 1
bird_list.remove(self)
else:
self.radius = self.health_point / 100.0
else:
# 寿命がなくなると死亡する
print('lifespan over')
lifespan_over_num += 1
bird_list.remove(self)
def draw(self):
pushMatrix()
translate(self.position[0], self.position[1])
rotate(self.direction)
stroke(255)
fill(self.col)
ellipse(0, self.radius, 5, 5)
ellipse(0, -self.radius, 5, 5)
ellipse(0, 0, self.radius * 2, self.radius * 2)
fill(0)
ellipse(self.radius * cos(PI / 6), self.radius * sin(PI / 6), 3, 3)
ellipse(self.radius * cos(-PI / 6), self.radius * sin(-PI / 6), 3, 3)
popMatrix()
class Feed:
def __init__(self, i, j):
self.x = i
self.y = j
self.feed_power = feed_power
self.eaten = False
def update(self):
if self.eaten:
if random.random() < feed_grow_rate:
self.eaten = False
def draw(self):
noStroke()
if self.eaten:
fill(FEED_COLORS[1])
else:
fill(FEED_COLORS[0])
rect(self.x * PATCH_SIZE, self.y * PATCH_SIZE, PATCH_SIZE, PATCH_SIZE)
# ベクトル操作の関数
def scale_vector(scalar, v):
return list(scalar * coord for coord in v)
生存空間を碁盤状に区切って、餌が生えてくるようにしました(50 x 50 = 250区画)。餌は鳥に食べられるとなくなってしまいますが、鳥は HPを feed_power(= 40)ポイント回復できるものとします。食べられた区画は feed_grow_rate(= 0.1)の確率で生えてきて、また餌が食べられる状態に戻ると仮定しました。
予想通り、鳥の生存期間は延びました。しかし鳥の寿命(200 から 500サイクル)があるため、個体数は徐々に減っていき最後は全滅してしまいました。
最後の条件として、鳥の繁殖を加えてみましょう。
鳥が繁殖できるようにする
class Bird:
def __init__(self, type_id=None):
"""鳥の初期化"""
...
def update(self):
...
# 鳥の体力がなくなると死亡する
if self.health_point <= 0:
print('health point over')
hp_over_num += 1
bird_list.remove(self)
else:
# 鳥の繁殖(追記)
if self.health_point > 700: # 追記
print('born') # 追記
born_num += 1 # 追記
self.health_point -= 200 # 追記
bird_list.append(Bird(self.type_id)) # 追記
self.radius = self.health_point / 100.0
Birdクラスの updateメソッドに繁殖のルールを加えました。餌が足りないため、初めは個体数が急減しますが、その後 200匹程度で安定させることができました。死亡(寿命とHPがなくなったことによる)と繁殖がバランスして、閉鎖空間での安定した集団を実現できました。
パラメータの変更(餌が少なくなる)
feed_grow_rate = 0.05
餌の復活率を下げて、より厳しい生存環境をシミュレートしました。予想通り、個体数は減少し、70匹程度で安定した集団となりました。
パラメーターの変更(餌が多くなる)
feed_grow_rate = 0.2
餌の復活率を上げて、より生きやすい環境にしてみました。予想通り、個体数は増えて、500匹程度で安定する集団を作りました。個体数の維持に対して、餌の供給が重要であることがわかります。
パラメーターを変更(個体差をつける)
# 餌の設定
feed_list = []
FEED_COLORS = [
'#006336', # Vert Cypres
'#986B35' # kuwatya
]
feed_grow_rate = 0.1
feed_power = 40
...
def draw():
...
# テキストを表示
fill(255)
textSize(30)
text('life cycle: ' + str(frameCount), 10, 30)
# text('bird num: ' + str(len(bird_list)), 10, 60)
# text('lifespan over: ' + str(lifespan_over_num), 10, 90)
# text('health point over: ' + str(hp_over_num), 10, 120)
# text('new born: ' + str(born_num), 10, 150)
text('pink: ' + str(len([bird for bird in bird_list if bird.type_id == 0])), 10, 60)
text('yellow: ' + str(len([bird for bird in bird_list if bird.type_id == 1])), 10, 90)
text('red: ' + str(len([bird for bird in bird_list if bird.type_id == 2])), 10, 120)
text('green: ' + str(len([bird for bird in bird_list if bird.type_id == 3])), 10, 150)
text('skyblue: ' + str(len([bird for bird in bird_list if bird.type_id == 4])), 10, 180)
text('white: ' + str(len([bird for bird in bird_list if bird.type_id == 5])), 10, 210)
text('lightpink: ' + str(len([bird for bird in bird_list if bird.type_id == 6])), 10, 240)
text('darkblue: ' + str(len([bird for bird in bird_list if bird.type_id == 7])), 10, 270)
class Bird:
def __init__(self, type_id=None):
"""鳥の初期化"""
self.position = [random.uniform(0, width), random.uniform(0, height)]
self.type_id = type_id if type_id else random.randint(0, 7)
# self.speed = PATCH_SIZE
self.speed = self.type_id * 10
...
次のシミュレーションは個体差をつけて、生存に最適な条件を探します。鳥の移動距離を変化させて、「pink < yellow < red < green < skyblue < white < lightpink < darkblue」の順に 10 ずつ大きくしました。ただし移動にはコストがかかり、HP(体力)が多く減ってしまうと仮定します。
1000サイクルの記録を取りましたが、yellow鳥の圧勝という結果になりました。これほどの差がつくとは予想外でした。移動に対してのメリット、デメリットがバランスするのが、yellow鳥(移動距離 10)との結果が得られました。
パラメーターの変更(餌の栄養価を上げる)
# feed_power = 40
feed_power = 60
最後のシミュレーションです。餌から得られるメリットを上げるため、餌の栄養価を $${40 \rightarrow 60}$$ に変更しました。結果は yellow鳥の圧勝で、1000匹以上の安定した集団を形成しました。移動距離の長い鳥が有利になると予想しましたが、ハズレでした。
はじめは red鳥が急速に増加しますが、120サイクルあたりで頭打ちになり減少していくのは興味深い現象です。原因は思いつかないのですが、どなたか分析できるでしょうか? コメント欄で教えてください。
まとめ
鳥と餌のシミュレーションを作成しました。パラメーターを変化させて、集団形成の分析を行うことが可能になりました。読者の皆さんも、いろいろパラメーターを変化させて遊んでみてくださいね。
前の記事
Processing でグラフを描く㉑ ボイドモデル
次の記事
Pythonでマイクラを作る ①Panda3Dの基礎
その他のタイトルはこちら