渡り鳥のVフォーメンション

渡り鳥が海を渡るときは、V字型のフォーメーションをとることはよく知られている。以前、コンピュータ上で鳥を仮想的に作って三つの規則を与えるとこのVフォーメンションを作ることができると聞いた。そこで調べてみると

Separation(分離)：鳥が他の鳥とぶつからないように一定の距離をとる。

Alignment(整列)：鳥が他の鳥と同じ方向に飛ぶように速度と方向を合わせる。

Cohesion(結合)：鳥が群れの中心へ向かうように方向を変える。

という３つの規則で実現できることが分かった。早速それを実現してみると確かに、鳥たちは驚くほど自然な動きを見せた。しかし、Vフォーメンションができない。そこでさらに調べてみると、このVフォーメンションは大きな渡り鳥に見られる現象で、どうも空気抵抗と揚力を綿密に計算して最適化しないと実現しないらしい。鳥たちのエネルギー消費を最小限に抑えるための編隊で、リーダーたちはかわるがわる交代しているようだ。これの精密な再現は難しいだろうと考えて、そうそう最新のChatGPT o1に相談してみた。３日間、提示されたコードを動かしても、Vフォーメンションは出てこない。そこで、ちょっと古いChatGPT4oに相談してみると３つの規則で実現できるということだ。

VフォーメンションになるということはCohesionはいらないのではと考えた。これが強いとどうしても集団が丸くなってしまう。つぎに、Alignmentを考える。鳥同士が速度を合わせる必要がるのだろうかと疑問に思った。実際、ここをゼロにしても大きな影響はなさそうだ。そうすると残るのはSeparationだけになった。これだと、初期値として与えられた位置をもとにさらにぶつからないようにそれぞれの鳥が離れるだけだ。ここで行き詰った。

ChatGPTの提示したコードを見ているとリーダーへの追随の強さというファクターがあるのに気が付いた。これだと思い、追随の強さと分離に的を絞って、変数を調整した。それなりの結果が出たように思う。

渡り鳥に、Vフォーメーションの空力の緻密な計算ができるわけない。でもたった２つのことを知っていれば実現できる。信頼するリーダーに追随する、そして距離を置く。

```Python
import numpy as np
import matplotlib.pyplot as plt
from numpy.random import Generator, PCG64, MT19937
import matplotlib
from matplotlib.font_manager import FontProperties
fp = FontProperties(fname=r'C:\WINDOWS\Fonts\meiryob.ttc', size=8)
 

rng = Generator(PCG64())#MT19937()) PCG64かMT19937(メルセンヌツイスター)を使用。
# パラメータ設定
N = 130  # 群れの個体数
width, height = 100, 100  # シミュレーション領域の大きさ
steps = 150  # シミュレーションステップ数
leader_index = 0  # リーダーのインデックス
alignment_strength = 0#0.05#/100  # 整列の影響力
cohesion_strength = 0#0.01#/1000 # 結合の影響力
separation_strength = 0.15#/0.2#/0.7  # 分離の影響力

# 初期設定
positions = rng.uniform(low=-width / 2, high=width / 2, size=(N, 2)) # 他の鳥の位置をリセット
positions[leader_index] = np.array([0, 0])  # リーダーの初期位置を(0, 0)に設定
velocities = rng.uniform(low=-1, high=1, size=(N, 2))  # ランダムな初期速度をリセット
initial_positions = positions.copy()  # 初期位置を保存
leader_positions = []  # リーダーの軌跡記録をリセット

# ターゲットの位置を100ステップで到達可能な距離に設定
average_speed = 5  # リーダーの最大速度に基づく推定値
target_distance = average_speed * 100  # 100ステップで到達する距離
target = np.array([target_distance / np.sqrt(2), target_distance / np.sqrt(2)])  # 対角方向に配置

# 更新関数


def update_positions_with_fixed_leader():
    global positions, velocities

    new_velocities = np.zeros_like(velocities)

    for i in range(N):
        if i == leader_index:
            # リーダーはターゲットに向かう
            direction_to_target = target - positions[i]
            distance_to_target = np.linalg.norm(direction_to_target)

            # 減速ロジック：ターゲットに近づくと速度を減らす
            if distance_to_target < 10:  # 距離が小さい場合
                scale_factor = 0.1  # 減速
            else:
                scale_factor = 0.5  # 通常速度

            # 速度変化を制限
            new_velocities[i] = velocities[i] + np.clip(
                scale_factor * direction_to_target / (distance_to_target + 1e-5),
                -1, 1
            )
        else:
            # フォロワーはBoidsのルールに従う
            neighbors = [j for j in range(N) if i != j and np.linalg.norm(positions[j] - positions[i]) < 10]

            # 整列
            alignment = np.mean(velocities[neighbors], axis=0) if neighbors else np.zeros(2)

            # 結合
            cohesion = np.mean(positions[neighbors], axis=0) - positions[i] if neighbors else np.zeros(2)

            # 分離
            separation = -np.sum([positions[j] - positions[i] for j in neighbors if np.linalg.norm(positions[j] - positions[i]) < 5], axis=0)

            # リーダーへの影響
            direction_to_leader = positions[leader_index] - positions[i]
            influence = 1.0 * direction_to_leader / (np.linalg.norm(direction_to_leader) + 1e-5)*500

            # 合計
            new_velocities[i] = velocities[i] + alignment_strength * alignment + cohesion_strength * cohesion + separation_strength * separation + influence

    # 位置と速度の更新
    velocities[:] = new_velocities
    velocities = np.clip(velocities, -2, 2)  # 速度をさらに厳しく制限
    positions[:] += velocities  # 境界条件を無効化
    leader_positions.append(positions[leader_index].copy())

# 初期速度をリセット
velocities[leader_index] = np.array([0, 0])  # リーダーの初期速度をゼロに固定
positions[leader_index] = np.array([0, 0])  # リーダーの初期位置を原点に固定
leader_positions = []  # リーダーの位置記録をリセット

# シミュレーション再実行
for step in range(steps):
    update_positions_with_fixed_leader()

# プロット作成
leader_positions_array = np.array(leader_positions)
plt.figure(figsize=(6, 6))

# リーダーの軌跡
plt.plot(leader_positions_array[:, 0], leader_positions_array[:, 1], linewidth=1, color="orange")
plt.text(leader_positions_array[75,0] + 10, leader_positions_array[75,1], s="The leader's tragectory", fontsize=10, fontproperties=fp)

# ターゲット
plt.scatter(target[0], target[1], color="red", marker="*", s=100)
plt.text(target[0] + 10, target[1], s="Target", fontsize=10, fontproperties=fp)

# 初期の群れの位置
plt.scatter(initial_positions[:, 0], initial_positions[:, 1], color="blue", alpha=0.5)
plt.text(initial_positions[0,0] + 60, initial_positions[0,1], s="initial position", fontsize=10, fontproperties=fp)

# 最終の群れの位置
final_positions = positions.copy()
plt.scatter(final_positions[:, 0], final_positions[:, 1], color="green", alpha=0.5,s=1)
plt.text(final_positions[0,0] + 20, final_positions[0,1], s="position in V-formation", fontsize=10, fontproperties=fp)

#plt.gca().tick_params(labelleft=False, labelbottom=False)  # ラベル非表示
#plt.gca().tick_params(left=False, bottom=False) 
#plt.axis('off')  # 軸全体を非表示
#plt.grid(False)  # マス目を非表示
#plt.title("渡り鳥が目的地に向かって飛ぶ様子byコンピューター・シミュレーション", fontproperties=fp)
plt.xlabel("longtitude", fontproperties=fp)
plt.ylabel("latitude", fontproperties=fp)
#plt.legend(prop=fp)
plt.grid()
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267055/f0e5303e-eec5-0a0f-ea0c-5f8775409cb6.png)