✈️

Pythonで航空機を飛ばす!AI時代のフライト制御入門

に公開

Pythonで航空機を飛ばす!AI時代のフライト制御入門[非AIエンジニアによるAI時代のコーディング]

はじめに:AIとPythonで航空機のフライト制御に挑戦!

皆さん、こんにちは!この記事では、「非AIエンジニアによるAI時代のコーディング」シリーズとして、Pythonを使って航空機のフライト制御の基礎を体験します。今回は、実際の航空機の経路設定でよく用いられる、あらかじめ設定された経路を自動で飛行する「ウェイポイント追従」というガイダンス手法にPID制御を組み合わせ、そのシミュレーションをPythonで実装します。今回も、CursorのAgent機能を主に使用しました。

今回のプロダクト:航空機の2次元ウェイポイント追従シミュレーション

今回開発するのは、2次元平面上を飛行する航空機が、あらかじめ設定されたウェイポイント(経由地点)を順番に辿っていくシミュレーションプログラムです。

このプログラムを使うことで、

  • 航空機がどのように目標地点に向かって飛行するのか
  • どのようなアルゴリズムで制御すれば、正確に経路を追従できるのか

といった、フライト制御の基本的な考え方を学ぶことができます。

シミュレーション結果は、アニメーションで表示されるので、航空機の動きを目で見て確認できるのも面白いところです。

プログラム解説:Pythonで航空機を制御してみよう!

それでは、Cursorが実際にPythonで書いたプログラムを見ていきましょう。

プログラム全体構成

このプログラムは、大きく分けて以下の要素で構成されています。

  1. Vehicleクラス: 航空機の状態(位置、速度、方位など)を管理する
  2. Controllerクラス: 航空機を目標地点に導くための制御アルゴリズム
  3. PIDControllerクラス: PID制御を行うためのクラス
  4. シミュレーション: 航空機の動きを時間経過とともに計算し、結果をアニメーションで表示する

Vehicleクラス

まずは、航空機の状態を管理するVehicleクラスを見てみましょう。

class Vehicle:
    def __init__(self):
        # 初期位置を最初のウェイポイントに設定
        self.x = 0  # 後で更新
        self.y = 0  # 後で更新
        self.v = 0  # 速度 [m/s]
        self.theta = 0  # 機首方位 [rad]

    def update(self, dt, acc, alpha):
        # 運動方程式に基づいて状態を更新
        self.v += acc * dt
        self.theta += alpha * dt
        self.x += self.v * np.cos(self.theta) * dt
        self.y += self.v * np.sin(self.theta) * dt

このクラスは、航空機の位置(x、y)、速度(v)、機首方位(theta)などの情報を保持し、時間経過に伴うこれらの変化を計算します。

Controllerクラス

次に、航空機を目標地点に導くための制御アルゴリズムを実装したControllerクラスを見てみましょう。

class Controller:
    def __init__(self):
        # PIDコントローラのゲイン設定
        self.pid_v = PIDController(0.5, 0.01, 0.1)    # 速度制御用PID
        self.pid_theta = PIDController(2.0, 0.01, 0.2) # 方位制御用PID
        self.target_v = 30  # 目標速度 [m/s]
        self.waypoint_index = 0
        self.arrival_radius = 5.0  # ウェイポイント到達判定半径 [m]
        self.slowdown_radius = 15.0  # 減速開始半径 [m]
        self.max_angle_error = np.pi/4  # 許容最大方位誤差 [rad]
        self.final_approach = False
        self.stop_decel = 1.0  # 停止時の減速度を緩和 [m/s²]

    def compute_control(self, vehicle, waypoints):
        if self.waypoint_index >= len(waypoints):
            # 最終地点での停止(緩やかに)
            if abs(vehicle.v) < 0.01:
                vehicle.v = 0
                return 0, 0
                
            # 方位制御を維持しながら緩やかに減速
            target = waypoints[-1]  # 最終ウェイポイント
            dx = target[0] - vehicle.x
            dy = target[1] - vehicle.y
            desired_theta = np.arctan2(dy, dx)
            
            # 方位誤差の計算
            theta_error = desired_theta - vehicle.theta
            theta_error = np.arctan2(np.sin(theta_error), np.cos(theta_error))
            
            # 方位制御は維持しながら減速
            alpha = self.pid_theta.update(theta_error)
            decel = -self.stop_decel if vehicle.v > 0 else self.stop_decel
            
            return decel, alpha
            
        # 現在の目標ウェイポイント
        target = waypoints[self.waypoint_index]
        
        # ウェイポイントまでの距離と方位を計算
        dx = target[0] - vehicle.x
        dy = target[1] - vehicle.y
        distance = np.sqrt(dx**2 + dy**2)
        desired_theta = np.arctan2(dy, dx)
        
        # 方位誤差の計算
        theta_error = desired_theta - vehicle.theta
        theta_error = np.arctan2(np.sin(theta_error), np.cos(theta_error))
        
        # 速度制御(方位誤差が大きい場合は減速)
        target_speed = self.target_v
        angle_factor = 1.0 - min(abs(theta_error) / self.max_angle_error, 1.0)
        
        if distance < self.slowdown_radius:
            # 距離に応じて線形に減速
            target_speed = max(5.0, self.target_v * (distance / self.slowdown_radius))
        
        # 方位誤差が大きい場合は速度を抑制
        target_speed *= max(0.3, angle_factor)
        
        # ウェイポイント到達判定
        if distance < self.arrival_radius:
            self.waypoint_index += 1
            print(f"Waypoint {self.waypoint_index} に到達")
            if self.waypoint_index >= len(waypoints):
                print("最終目的地に到達、停止します")
                self.final_approach = True
                # 最終接近時は一定の減速度で停止
                decel = -self.stop_decel if vehicle.v > 0 else self.stop_decel
                return decel, 0
        
        # PID制御による制御入力の計算
        v_error = target_speed - vehicle.v
        
        acc = self.pid_v.update(v_error)
        alpha = self.pid_theta.update(theta_error)
        
        # 制御入力の制限
        acc = np.clip(acc, -5, 5)
        alpha = np.clip(alpha, -1, 1)

        return acc, alpha

このクラスは、PID制御を用いて、航空機への制御入力(加速度 acc、方位変化率 alpha)を計算します。

  1. PIDController: 速度と方位の制御にPID制御を使用。
  2. target_v: 目標速度
  3. waypoint_index: 現在目標としているウェイポイントのインデックス
  4. arrival_radius: ウェイポイント到達判定半径
  5. slowdown_radius: 減速開始半径
  6. max_angle_error: 許容最大方位誤差
  7. final_approach: 最終接近フラグ
  8. stop_decel: 停止時の減速度

PIDControllerクラス

Controllerクラスで使用されているPID制御を行うPIDControllerクラスを見てみましょう。

class PIDController:
    def __init__(self, Kp, Ki, Kd):
        self.Kp = Kp
        self.Ki = Ki
        self.Kd = Kd
        self.integral = 0
        self.prev_error = 0

    def update(self, error):
        self.integral += error
        derivative = error - self.prev_error
        self.prev_error = error
        return self.Kp * error + self.Ki * self.integral + self.Kd * derivative

このクラスは、PID制御のゲイン(Kp、Ki、Kd)を設定し、誤差に基づいて制御量を計算します。

シミュレーション

シミュレーション部分では、航空機の状態を時間経過とともに更新し、その結果をアニメーションで表示します。

def simulate_waypoint_following():
    # 初期化
    vehicle = Vehicle()
    controller = Controller()
    
    # 日本の主要空港の座標と名前
    R = 6371000
    origin = [135.0, 35.0]
    airports = [
        ([139.7819, 35.5489], "羽田空港"),
        ([136.9647, 35.2550], "名古屋空港"),
        ([135.2440, 34.7850], "関西空港"),
        ([132.9194, 34.4361], "広島空港"),
        ([130.4514, 33.5847], "福岡空港"),
        ([130.7189, 31.8034], "鹿児島空港")
    ]
    
    waypoints = np.array([p[0] for p in airports])
    
    # 緯度経度を平面直角座標に変換
    waypoints = np.array([
        [R * np.cos(np.radians(p[1])) * np.radians(p[0] - origin[0]),
         R * np.radians(p[1] - origin[1])] 
        for p in waypoints
    ]) * 0.0002

    # 初期位置を最初のウェイポイントに設定
    vehicle.x = waypoints[0][0]
    vehicle.y = waypoints[0][1]
    controller.waypoint_index = 1  # 2番目のウェイポイントから目指す

    # アニメーションの設定
    plt.rcParams['font.family'] = 'Arial'  # フォントをArialに設定
    fig, ax = plt.subplots(figsize=(10, 10))
    ax.set_xlim(-100, 100)
    ax.set_ylim(-100, 100)
    ax.grid(True)
    ax.set_xlabel('X [m]')
    ax.set_ylabel('Y [m]')
    ax.set_title('航空機のウェイポイント追従シミュレーション', fontname="MS Gothic")  # 日本語フォントを指定
    
    # ウェイポイントのプロットと名前の表示
    waypoint_markers = []  # ウェイポイントのマーカーを保存
    radius_circles = []    # 到達判定半径の円を保存
    
    # 初期状態でのウェイポイントと到達半径の表示
    for i, (coord, name) in enumerate(zip(waypoints, [a[1] for a in airports])):
        # ウェイポイントのマーカー
        marker, = ax.plot(coord[0], coord[1], 'r*', markersize=10, label='Waypoints' if i == 0 else "")
        waypoint_markers.append(marker)
        
        # 到達判定半径の円
        circle = plt.Circle((coord[0], coord[1]), controller.arrival_radius, 
                          fill=False, linestyle='--', color='g', alpha=0.5)
        ax.add_patch(circle)
        radius_circles.append(circle)
        
        # ウェイポイント名の表示
        ax.annotate(name, (coord[0], coord[1]), 
                   xytext=(5, 5), textcoords='offset points',
                   fontname="MS Gothic", fontsize=8)
    
    # ウェイポイント間を結ぶ線
    ax.plot(waypoints[:,0], waypoints[:,1], 'r--', alpha=0.3)
    
    # 機体の軌跡と現在位置
    trajectory, = ax.plot([], [], 'b-', label='Trajectory')
    position, = ax.plot([], [], 'ko', label='Aircraft')
    
    # 情報表示用のテキスト(位置を調整)
    info_text = ax.text(-95, 85, '', fontsize=10, fontname="MS Gothic",
                       bbox=dict(facecolor='white', alpha=0.7))  # 背景を追加して見やすく
    
    # 凡例の表示
    ax.legend()
    
    def init():
        trajectory.set_data([], [])
        position.set_data([], [])
        return trajectory, position, info_text, *waypoint_markers, *radius_circles
    
    trajectory_x = []
    trajectory_y = []
    
    def animate(frame):
        if frame >= 200:
            anim.event_source.stop()
            return trajectory, position, info_text, *waypoint_markers, *radius_circles
            
        dt = 0.1
        acc, alpha = controller.compute_control(vehicle, waypoints)
        vehicle.update(dt, acc, alpha)
        
        # 軌跡データの更新
        trajectory_x.append(vehicle.x)
        trajectory_y.append(vehicle.y)
        
        # プロットの更新
        trajectory.set_data(trajectory_x, trajectory_y)
        position.set_data([vehicle.x], [vehicle.y])
        
        # 到達済みウェイポイントの色を変更
        for i in range(controller.waypoint_index):
            if i < len(waypoint_markers):
                waypoint_markers[i].set_color('blue')  # 到達済みは青色に
                radius_circles[i].set_color('gray')    # 円も灰色に
        
        # 情報表示の更新
        elapsed_time = frame * dt
        current_waypoint = "到着済" if controller.waypoint_index >= len(airports) else airports[controller.waypoint_index][1]
        
        # 方向を0-360度で表示(負の角度を正の角度に変換)
        direction = np.degrees(vehicle.theta) % 360
        
        info = f'時間: {elapsed_time:.1f}秒\n'
        info += f'位置: ({vehicle.x:.1f}, {vehicle.y:.1f})\n'
        info += f'速度: {vehicle.v:.1f} m/s\n'
        info += f'方向: {direction:.1f}°\n'
        info += f'次の目的地: {current_waypoint}'
        if controller.final_approach:
            if abs(vehicle.v) < 0.01:
                info += '\n状態: 停止完了'
            else:
                info += '\n状態: 停止中...'
        
        info_text.set_text(info)
        info_text.set_fontname("MS Gothic")
        
        return trajectory, position, info_text, *waypoint_markers, *radius_circles
    
    # アニメーションの実行
    anim = FuncAnimation(fig, animate, init_func=init,
                        frames=None, interval=50, blit=True)

    plt.show()

matplotlibのFuncAnimationを使用することで、航空機の動きを滑らかなアニメーションとして表示することができます。

シミュレーションの結果

今回のプログラムを実行すると、以下のようなシミュレーション結果が出力されました。

経路追従はほぼ再現できていますね。
惜しいのは、終着地点で減速し切れていないところです。ここは改善の余地がありそうです。

AI活用のポイント:PID制御と経路計画

今回のプログラムでは、航空機の制御にPID制御という古典的な制御手法を使用しています。
PID制御は、

  • P制御 (比例制御): 目標値との誤差に比例した制御を行う
  • I制御 (積分制御): 定常偏差を解消する
  • D制御 (微分制御): 変化の速度を抑制する

という3つの要素を組み合わせることで、安定した制御を実現する手法です。
AI技術を活用することで、

  • PID制御のゲインを自動的に調整する
  • より複雑な経路計画アルゴリズムを実装する
  • 気象データなどの外部環境を考慮した制御を行う

といった、さらに高度なフライト制御が可能になります。

今後の可能性

今回のプログラムは、シンプルな2次元空間でのシミュレーションですが、実際の航空機制御では、

  • 3次元空間での飛行
  • 乱気流などの外乱への対応
  • 他の航空機との衝突回避

など、さまざまな要素を考慮する必要があります。
AI技術を活用することで、これらの複雑な課題を解決し、より安全で効率的な航空交通システムを構築することが可能です。
それにしても、AIだけでここまでのプログラムが開発できるのは、シビれますね。

Discussion