人工生命クランの開発　その２

# 目標
「人工生命クランの開発　その１」では、移動して、食事して、増殖して、死んでいく人工生命らしきものを作成できました。

https://qiita.com/Hakoshi/items/96359659d7922e290366

```python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.animation import PillowWriter

WIDTH = 5
HEIGHT = 5
CYCLES = 20

fig, ax = plt.subplots(figsize=(6,6))

# Cellクラス plt.Circleを使って円形のパッチを作成、体力に比例した大きさで描画
class Cell:
    def __init__(self, x, y, col='white', life=5, speed=1, threshold=10):
        self.x = x
        self.y = y
        self.col = col
        self.life = life
        self.speed = speed
        self.threshold = threshold
        # 円形パッチとして生成し、軸に追加する
        self.circle = plt.Circle((self.x, self.y), radius=0.1*self.life, facecolor=self.col)
        self.circle.set_zorder(10) # フィールドより前面に描画する

    def update(self):
        # １サイクル進むと体力が1減る。
        self.life -= 1
        
        # 体力が残っていればセルが移動、食事、分裂。
        if self.life > 0:
            self.move()
            self.eat()
            self.divide()

        # セルの位置、大きさを変更する
        self.circle.center = (self.x, self.y)
        self.circle.set_radius(0.1 * self.life)
        
        return self.circle

    # セルが移動する。±speedの範囲内でランダムに移動する
    def move(self):
        self.x += np.random.randint(-self.speed, self.speed + 1)
        self.y += np.random.randint(-self.speed, self.speed + 1)

        # numpy の np.clip を使って範囲内に収めた上で、整数に変換
        self.x = int(np.clip(self.x, 0, WIDTH))
        self.y = int(np.clip(self.y, 0, HEIGHT))

    # フィールドから養分をとる
    def eat(self):
        # 現在のセルの位置に対応するフィールドを fieldList からインデックスを用いて取得
        index = self.y * (WIDTH + 1) + self.x
        field_obj = fieldList[index]
        
        # 対応するフィールドの life が 1 であれば、フィールドの life を減らし、そのフィールドの energy 値をセルの life に加える
        if field_obj.life == 1:
            field_obj.life -= 10
            self.life += field_obj.energy

    # 体力がthresholdを越えたら分裂する
    def divide(self):
        if self.life >= self.threshold:
            # 分裂前に体力を半分に減らす
            self.life //= 2
            # 親セルと同じ座標に子セルが出現。cellListに加える。
            cellList.append(Cell(x=self.x, y=self.y))


# Fieldクラス
# 座標、サイズ、エネルギー、ライフがあり、デフォルトの色は緑、正方形に描画する
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_zorder(1)
        # 矩形パッチとして生成し、軸に追加
        ax.add_patch(self.rectangle)

    def update(self):
        # フィールドが、セルによって養分を取られて、lifeが1より下になると、茶色に変色する。サイクル毎にlifeが回復する。
        if self.life < 1:
            self.rectangle.set_facecolor('brown')
            self.life += 1
        # それ以外の場合（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))


# Cellオブジェクトを作成する
cellList = [Cell(2, 2), Cell(3, 3)]

# animate関数　各アニメーションフレームごとに呼ばれ、描画クリアと再設定を行なう。
def animate(i):
    # まず前のフレームの描画をクリアする
    ax.clear()
    # 表示領域のX軸・Y軸の範囲を設定。
    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(f"Cycle {i}")

    # 初回のフレームでは初期状態をそのまま表示する
    if i == 0:
        for cell in cellList:
            ax.add_patch(cell.circle)
        for field in fieldList:
            ax.add_patch(field.rectangle)
    else:
        dead_cells = [] # 削除対象となるセルを一時的に保管するための空リスト
        for cell in cellList:
            # 各セルに対してupdate()が呼ばれ、体力が減り、移動・食事・分裂して位置と大きさを更新
            # 更新されたパッチを再度ax.add_patchで追加され、最新の状態が描画される
            ax.add_patch(cell.update())
            if cell.life < 1:
                dead_cells.append(cell) # 体力が尽きるとdeadCellsに追加される。
        for cell in dead_cells:
            cellList.remove(cell) # 体力が尽きたセルは今後のフレームで描画・更新されなくなる

        for field in fieldList:
            ax.add_patch(field.update()) # 各フィールドに対してupdate()が呼ばれる

    # 現在のフレームで表示されている全てのパッチを返す。
    return ax.patches

# 上記のanimate関数を各フレームで実行してアニメーションを生成する
ani = animation.FuncAnimation(fig, animate, frames=CYCLES, blit=False)

writer = PillowWriter(fps=2)
ani.save("animation.gif", writer=writer)
plt.close(fig)  # 保存後に閉じる

```
![animation (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3999109/7cec9cde-a701-443b-82bc-ab759137cf2e.gif)



しかし、毎回 ax.clear() を呼んでいるということで、軸設定を毎フレーム再設定してさらにすべてのパッチを1から追加しているため、コードは簡潔明瞭ですがパフォーマンスが低下します。実際、フィールドやサイクルを大きくすると、実行するのに10分かかったりしてイライラします。
Copilotに解決方法を尋ねてみると、blitオプションを活用して更新が必要な部分だけを描画するように工夫しろ、とのことでした。

# blittingについて
blittingとは、Matplotlibで描画を高速化するための技術です。通常、アニメーションでは毎フレームごとに画面をクリアして全てのオブジェクトを描画しますが、blittingを使うと変化があった部分だけを再描画することで処理を軽量化できるのです。

### Blittingの仕組み
1. 背景画像を事前にキャッシュ: 画面の固定部分（例えば軸や背景）を一度描画し、その状態を保存します
2. 動的なオブジェクトのみを再描画: アニメーションの更新時、変更があったオブジェクトだけを描画します
3. 背景とオブジェクトを合成（blit）: 以前キャッシュした背景に変化したオブジェクトを合成します

ただし、初期化時に返されたアーティスト（描画オブジェクト）のリストがフレーム間で固定されていることが前提となります。どういうことかというと、このコードでは分裂や死亡によりcellListにセルが追加・削除されているため、各フレームで返すアーティストの数が変化し、blittingの仕組みと整合性が崩れてしまうため、工夫が必要になってきます。

# Cellクラスを変更する
セルの数を固定するための工夫として、あらかじめ大量の（フィールドと同じ数にしておきます）の不活性状態のセルを作成しておいて、後から活性状態に切り替えて利用します。死んだら不活性化状態とし、分裂して新たに出現したら活性化するという運用です。
これまではanimate関数内でセルリストの管理をしていましたが、update()で完結できます。

```python
# Cellクラスを変更する
class Cell:
    def __init__(self):
        # 初期状態は非活性にしておく
        self.active = False
        self.x = 0
        self.y = 0
        self.col = 'white'
        self.life = 0
        self.speed = 1
        self.threshold = 10
        # 描画用の円パッチ。初期は半径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=5, speed=1, threshold=10):
        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  # 1つだけ活性化すればループを抜ける

```
# Fieldクラスの変更
ほとんど変更するところはなく、__init__にset_animated(True)を入れるだけです。

```python
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
```
# セルとフィールドのリスト
フィールドリストは変更ありません。
セルについては、フィールドと同じ数のセルプールを作成し、不活性化状態でセルを作成しておきます。
初期配置の数だけ活性化します。
処理の効率がよくなっているはずなのでHEIGHTとWIDTHを大きくしています。セルはランダムに配置しています。

```python

WIDTH = 30
HEIGHT = 30
CYCLES = 300
DENSITY = 0.01

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)
```

# 初期化関数、animate関数

```python
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) # タイトルオブジェクトをアニメーション用としてマーク

    # アニメーション対象のアーティストをリストにまとめる。タイトル、フィールド、セル。
    artists = [field.rectangle for field in fieldList] + [cell.circle for cell in cellPool]
    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()

    artists = [ax.title] + [field.rectangle for field in fieldList] + [cell.circle for cell in cellPool]
    return artists

# blit=True として FuncAnimation を生成
ani = animation.FuncAnimation(fig, animate, frames=CYCLES, init_func=init, blit=True)
```

![animation (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3999109/f4e9835a-f827-4f98-8364-032d0e0d3322.gif)


# セルの数の推移を折れ線グラフで描画する
blittingを活用することで、世界を大きく、さらに歴史も長くできるようになりました。上の画像は、
WIDTH = 30
HEIGHT = 30
CYCLES = 300
で作っています。そのため、セルの数が多くなって把握しきれなくなりました。というわけで、セルの数の推移を折れ線グラフで描画しようと思います。
方針としては、セルやフィールドと同じように、折れ線グラフクラス（LineGraphクラス）を作成して、animate関数内でupdateします。線を描画する場合には、X軸のデータとY軸のデータをまとめる必要があります。

```python
# 折れ線グラフのための軸（ax_graph）を作る
fig, (ax, ax_graph) = plt.subplots(1, 2, figsize=(12,6))


# シミュレーション中のアクティブなセルの数の履歴をグラフとしてプロットするためのクラス
class LineGraph:
    # セルオブジェクトのリストを受け取る。
    def __init__(self, cellPool):
        self.cellPool = cellPool
        # activeCellCountの初期値としてリストを用意する。リストの最初の要素には、初期値の活性化されたセルの数が保存される。
        self.activeCellCount = [sum(cell.active for cell in self.cellPool)]

        x_data = np.arange(len(self.activeCellCount)) # ここでは[0]
        # プロットが返す Line2D オブジェクトをクラスのインスタンス変数に保存
        self.line, = ax_graph.plot(x_data, self.activeCellCount, animated=True)

    def update(self):
        # 毎回 update メソッドが呼ばれるたびに、活性化されているセルの数が計算され、self.activeCellCount のリストに追加
        self.activeCellCount.append(sum(cell.active for cell in self.cellPool))
        # 更新後のリストの長さに合わせた x 軸データが生成
        x_data = np.arange(len(self.activeCellCount))
        self.line.set_data(x_data, self.activeCellCount) 
        return self.line

# セルのリスト 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')

    # アーティストにlineGraph.lineを追加する
    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する
        lineGraph.update()

    artists = [ax.title] + [field.rectangle for field in fieldList] + [cell.circle for cell in cellPool] + [lineGraph.line]
    return artists
```

![animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3999109/bda7aeb4-b228-4d6f-888e-009a2cca6039.gif)

できました。
次はセルの変異を実装したいと思います。