1969年、Apollo 11号の月着陸船「イーグル」は、最終降下フェーズで予想外の岩場に進路を見出だせず、ニール・アームストロング船長が手動操縦に切り替えて着陸点を選び直しました。残燃料はわずか25秒分でした。半世紀後の今、再び月面に降りようとする我々は、もっと冷静に、もっと最適に、もっと頑健に降下したいと考えます。月面パワード降下(Powered Descent)の問題は、限られた燃料で、推力の大きさ・向きが制約された推進系を使い、目的地に軟着陸するという、宇宙工学の中でも最も難しい連続制御問題のひとつです。
この問題に対する古典的な解は、Apollo以来のE-Guidance(Explicit Guidance, Bennett 1965)や、近年のGFOLD(G-FOLD: Guidance for Fuel-Optimal Large Diverts, Açıkmeşe & Ploen 2007)といった凸最適化に基づく軌道計画でした。しかし、月面の不確実性(地形、慣性、推力の劣化)、リアルタイムでの再計画、推力大きさ・方向の鋭い制約、そして「ガス再点火不可」のような物理的に厳しい運用制約を全て満たす制御則を、解析的・数値的に手作りで完結させるのは限界に近づきつつあります。
そこで登場したのが深層強化学習(Deep Reinforcement Learning, DRL) によるパワード降下制御です。本記事は、次の問いに正面から答えることを目指します。
- なぜ強化学習なのか? Apollo G&NやGFOLDに対して、RLが本質的に提供できるものは何か。
- どう定式化するか? ロケット方程式から始めて、燃料最適制御問題をMDPに翻訳する手続きを丁寧に追います。
- どう実装するか? numpyで1D/2Dの月面着陸環境を作り、PyTorchでDDPG/SACの簡易版を学習させ、燃料消費・着地誤差・推力プロファイルを可視化します。
応用先は、月面着陸(Artemis計画のヒューマンランダーや民間月着陸船)にとどまりません。火星EDLの最終フェーズ(SkyCrane、後継ランダー)、再使用ロケットの逆推進着陸(Falcon 9/Starshipの逆噴射制御)、小惑星サンプル採取機の自律タッチダウン、さらには地球大気圏ベーシック降下を超えるサブオービタル輸送の最終着地など、「推力で減速して軟着陸する」全ての宇宙機の自律GNC に直結する技術です。
本記事の内容
- なぜパワード降下にRLか — 不確実性・最適性・リアルタイム再計画という3つの観点
- ロケット方程式と燃料最適制御 — Tsiolkovsky方程式から最大原理まで
- Apollo G&N、E-Guidance、ZEM/ZEVの幾何学的アイデア
- 凸最適化アプローチ(GFOLD)との比較とRLが補う領域
- MDP定式化 — 状態(位置・速度・質量)、行動(推力ベクトル)、報酬(着地誤差・燃料・安全制約)
- TD3 / SAC / メタRLの位置付け
- 連続制御の課題 — 軟着陸制約、推力大きさ制約、ガス再点火不可
- numpyによる1D月面着陸環境とPyTorchによるDDPG簡易実装
- 燃料・着地誤差・推力プロファイルの可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
- 強化学習の基礎 — MDPとベルマン方程式をわかりやすく解説
- 軌道力学の基礎 — 軌道要素とケプラー運動
- 最適制御入門 — ハミルトニアンとポントリャーギンの最大原理
- DDPGとTD3 — 連続行動空間の決定的方策勾配
なぜパワード降下に強化学習なのか
Apollo時代の発想と限界
月面パワード降下というタスクを、まずApolloがどう解いたかから振り返りましょう。1960年代半ばにMITが開発したApollo G&N(Guidance and Navigation)システムは、当時の限られた計算機資源で、燃料を浪費せず、かつ手動オーバーライドが可能な誘導則を要求されていました。Bennettらが提案した E-Guidance は、目標着地点までの残時間 $t_{\text{go}}$ を陽に推定し、現在の位置・速度から「目標で速度・加速度を所望の値にする」ような目標加速度を多項式の係数として計算する、極めてエレガントな手法です。
E-Guidanceの典型的な指令加速度は、目標までの残時間 $t_{\text{go}}$ と現在位置 $\bm{r}$、速度 $\bm{v}$、目標位置 $\bm{r}_f$、目標速度 $\bm{v}_f$ から、以下のような形で書けます。
$$ \begin{equation} \bm{a}_{\text{cmd}} = \frac{6(\bm{r}_f – \bm{r})}{t_{\text{go}}^2} – \frac{4 \bm{v} + 2 \bm{v}_f}{t_{\text{go}}} + \bm{a}_g \end{equation} $$
これは「現在状態から目標状態へ向かう多項式軌道」を満たす指令加速度であり、$\bm{a}_g$ は重力補償項です。この単純な式が、Apollo 11号を月面に下ろした立役者でした。
しかし、E-Guidanceには次のような限界があります。
- 燃料最適でない: 多項式軌道に従わせるための解析解であり、最少燃料を直接目的にしていません。
- 不確実性に弱い: 着陸船質量の見積もり誤差、エンジン推力の劣化、月面重力場の精細な変動などに対して、再計画機能が組み込まれていません。
- 制約の扱いが粗い: 推力大きさの上限、姿勢の制約、月面接触時の速度制約などは、誘導則の外でアドホックに処理されます。
結局Apollo 11では、着陸点が予想と違っていたために、アームストロング船長が人間の判断で最終フェーズを引き受けることになりました。AIによる月面降下制御の出発点はまさにここ、「人間に手綱を渡さなければならなかった瞬間」を機械でどうカバーするか、という問いです。
GFOLDによる凸最適化アプローチ
2000年代後半、Açıkmeşe らはこの問題を 凸最適化 で解く手法を体系化しました。GFOLDは、Powered Descent Guidance(PDG)問題を、ロスレス凸化(Lossless Convexification)という変換を経て、有限次元の二次錐計画問題(SOCP)に書き直します。これにより、燃料最適な軌道を、内点法で信頼性高く実時間に近い時間で解くことができるようになりました。GFOLDはSpaceXのFalcon 9の着陸成功の理論的基盤の一部でもあると言われています。
GFOLDのキー・アイデアは、推力ベクトル $\bm{T}(t)$ の大きさ制約 $T_{\min} \le \|\bm{T}\| \le T_{\max}$ という非凸制約を、スラック変数 $\Gamma(t)$ を導入して
$$ \|\bm{T}(t)\| \le \Gamma(t), \quad T_{\min} \le \Gamma(t) \le T_{\max} $$
と書き換えることで、凸計画として定式化することです。これは数学的に等価であり、しかも凸性を満たします。
ただし、GFOLDにも限界があります。
- モデルベース: ダイナミクスを正確に書き下せる前提が必要。月面重力場の高次項、地形誘起の微妙な擾乱、エンジン応答の非線形性などは線形近似されます。
- 離散時刻でのオープンループ最適化: 厳密にはMPCのように毎ステップ再解する必要があり、計算負荷とのトレードオフが発生します。
- 制約の組み合わせ爆発: 観測ベースの地形回避(hazard avoidance)、姿勢拘束、視線ベクトル制約などを増やすと、凸性を維持するためのトリックが累積します。
つまり、E-Guidanceは「解析的だが粗い」、GFOLDは「最適だがモデルベースで重い」、というトレードオフの関係にあります。次に、強化学習がこのトレードオフのどこに位置するのかを見ていきましょう。
強化学習が補う3つの軸
深層強化学習は、E-GuidanceとGFOLDの間と外を埋めるポテンシャルを持ちます。具体的には次の3つの軸で利点があります。
1. 不確実性への適応: 月面着陸では、着陸船の慣性テンソルが想定とずれる、エンジン推力が想定の95%しか出ない、月面の標高モデルに数十mの誤差がある、といった様々な不確実性が常に存在します。RLは学習時にこれらの不確実性をドメインランダマイゼーションとして組み込むことができ、未知の状況にも頑健な方策を獲得できます。
2. 計算負荷の前倒し: GFOLDのような最適化ベースの誘導則は、推論時に毎周期SOCPを解く必要があります。RLは学習時に膨大な計算を投じる代わりに、推論時はニューラルネットの1回の順伝播で済みます。月の宇宙環境(放射線で計算資源が地上の数百分の一)では、この性質は実装上の決定的なアドバンテージです。
3. リアルタイム再計画と非凸制約の自然な扱い: ハザード回避、推力ON/OFFのデジタル制約、姿勢の不連続な切り替えなどは、本質的に非凸です。RLは報酬関数を通じてこれらを柔軟に扱え、しかも観測から方策までエンドツーエンドに最適化できます。
強化学習が「Apollo誘導を越える」ことの本質は、最適性・頑健性・計算効率を同時に高水準で実現できる可能性 にあります。次のセクションでは、この主張を数式的に支える基礎として、ロケット方程式と燃料最適制御問題を整理しましょう。
ロケット方程式と燃料最適制御問題
Tsiolkovsky方程式の復習
宇宙機が推進剤を噴射して加速するとき、推進剤の噴射速度 $v_e$ と機体質量変化の関係から、Tsiolkovsky のロケット方程式 が導かれます。
$$ \begin{equation} \Delta v = v_e \ln \frac{m_0}{m_f} \end{equation} $$
ここで $\Delta v$ は機体が獲得する速度増分、$m_0$ は推進剤を含む初期質量、$m_f$ は終端質量です。月面着陸では、初期質量に対して必要な $\Delta v$ がほぼ決まっているので、$v_e$ を大きくするか、初期質量を大きくするか、それとも「無駄な $\Delta v$ を費やさないようにする」しかありません。前者2つはハードウェア設計、後者は誘導制御の問題です。
直感的には、$\ln$ が登場することが効いています。燃料の節約は線形ではなく対数的に効くため、最終フェーズに少しだけ余分に推力を吹かすことが、後段の燃料・質量・着陸性能に大きな影響を与えるのです。
パワード降下の運動方程式
月面に向けて降下する宇宙機の運動方程式は、月固定座標系で書くと次のようになります。
$$ \begin{equation} \dot{\bm{r}} = \bm{v}, \quad \dot{\bm{v}} = \bm{g} + \frac{\bm{T}}{m}, \quad \dot{m} = -\frac{\|\bm{T}\|}{v_e} \end{equation} $$
ここで $\bm{r}, \bm{v}$ は位置と速度、$m$ は質量、$\bm{T}$ は推力ベクトル、$\bm{g}$ は月の重力加速度(地球の約1/6, $\|\bm{g}\| \approx 1.62 \, \text{m/s}^2$)、$v_e$ は有効排気速度($v_e = I_{sp} g_0$, $I_{sp}$ は比推力)です。
第1式と第2式は通常のニュートン力学、第3式は質量損失率がスラスト大きさに比例することを表します。質量が状態変数として動く ところが、この問題が単純な線形制御問題と一線を画す点です。
制御入力には次の制約があります。
$$ T_{\min} \le \|\bm{T}\| \le T_{\max} $$
ここで $T_{\min}>0$ なのは、多くの液体推進エンジンが一度点火すると一定以上のスロットルを維持しないと不安定になるためです。月面ランダーの場合、たとえばApollo LM Descent Engineは10%〜100%スロットルが可能でしたが、それでもゼロにはできませんでした。これが後述する「ガス再点火不可」「下限スラスト制約」を生み出します。
燃料最適制御問題(FOPDG)の定式化
パワード降下の燃料最適制御問題(Fuel-Optimal Powered Descent Guidance, FOPDG)は、次のように書けます。
$$ \begin{equation} \min_{\bm{T}(\cdot)} \quad \int_{0}^{t_f} \|\bm{T}(t)\| \, dt \end{equation} $$
ただし制約として、
$$ \dot{\bm{r}} = \bm{v}, \quad \dot{\bm{v}} = \bm{g} + \bm{T}/m, \quad \dot{m} = -\|\bm{T}\|/v_e $$
$$ T_{\min} \le \|\bm{T}\| \le T_{\max}, \quad m(t) \ge m_{\text{dry}} $$
$$ \bm{r}(0) = \bm{r}_0, \quad \bm{v}(0) = \bm{v}_0, \quad m(0) = m_0 $$
$$ \bm{r}(t_f) = \bm{r}_f, \quad \bm{v}(t_f) = \bm{v}_f $$
を満たすこと。これは「初期状態から終端状態まで、推力大きさの積分(燃料消費)を最小化せよ」という問題です。
ここで重要なのは、$\int \|\bm{T}\| dt$ が直接的には総推力ではなく、ロケット方程式により $\Delta m = \frac{1}{v_e}\int \|\bm{T}\| dt$ なので、消費推進剤質量に比例する という点です。だから「推力の大きさの積分」を最小化することは「燃料消費を最小化」することと等価になります。
最大原理が示すBang-Bang構造
この問題にポントリャーギンの最大原理を適用すると、最適推力プロファイルがBang-Bang型(推力大きさは $T_{\min}$ か $T_{\max}$ のいずれか)になることが導かれます。ハミルトニアン
$$ H = \|\bm{T}\| + \bm{\lambda}_r^T \bm{v} + \bm{\lambda}_v^T (\bm{g} + \bm{T}/m) + \lambda_m (-\|\bm{T}\|/v_e) $$
を $\bm{T}$ で最小化すると、推力方向は $-\bm{\lambda}_v$ 方向に、大きさはスイッチング関数
$$ S(t) = 1 – \frac{\|\bm{\lambda}_v(t)\|}{m(t)} \cdot v_e + \lambda_m(t)/v_e $$
の符号に応じて $T_{\min}$ か $T_{\max}$ となる、というのが古典的な結果です。
つまり、燃料最適な月面降下は、推力をフル吹かすか最小に絞るかを切り替えるBang-Bang制御 なのです。GFOLDが解いているのは、まさにこのスイッチング時刻を含んだ凸化された問題に他なりません。
この最適制御の理論的構造を踏まえると、RLが学ぶべき方策の形がイメージできます。決定的方策(DDPG/TD3)であれば、状態を入力に推力ベクトルを直接出力する関数を、Bang-Bang構造を含む形で表現する必要があります。確率的方策(SAC)であれば、エントロピー正則化が探索を助けつつ、最終的にBang-Bangに近い決定的振る舞いに収束していく構造です。
次のセクションでは、この連続最適制御問題をマルコフ決定過程(MDP)として書き直す手続きを丁寧に追います。
MDPへの定式化
状態・行動・遷移の設計
連続時間の最適制御問題をRLの枠組みに落とすには、時間を離散化 し、状態・行動・遷移確率・報酬 を明示的に定義する必要があります。月面パワード降下のMDPを次のように設計しましょう。
状態 $\bm{s}_t$: 着陸船の運動学・力学量を全て含みます。
$$ \bm{s}_t = [\bm{r}_t, \bm{v}_t, m_t, t_{\text{go},t}]^T \in \mathbb{R}^{8} $$
3D問題なら $\bm{r}, \bm{v} \in \mathbb{R}^3$ なので $3+3+1+1 = 8$ 次元、1D問題(垂直降下のみ)なら $1+1+1+1 = 4$ 次元です。$t_{\text{go}}$(残時間)を状態に含めるか否かは設計の選択ですが、含めると Markovian な定式化が綺麗になります。
行動 $\bm{a}_t$: 推力ベクトル。
$$ \bm{a}_t = \bm{T}_t \in \mathbb{R}^3 $$
実装上は、推力の大きさ $\|\bm{T}\|$ と方向ベクトルに分解して2つの行動出力にすることもあります。連続行動空間なので、DDPG/TD3/SACが自然な選択になります。
遷移 $P(\bm{s}_{t+1}\mid\bm{s}_t, \bm{a}_t)$: 前節の運動方程式を時間ステップ $\Delta t$ で離散化します。オイラー法だと次のようになります。
$$ \bm{r}_{t+1} = \bm{r}_t + \bm{v}_t \Delta t $$
$$ \bm{v}_{t+1} = \bm{v}_t + \left(\bm{g} + \frac{\bm{a}_t}{m_t}\right) \Delta t $$
$$ m_{t+1} = m_t – \frac{\|\bm{a}_t\|}{v_e} \Delta t $$
$$ t_{\text{go}, t+1} = t_{\text{go}, t} – \Delta t $$
センサノイズや擾乱を加えるなら、$\bm{r}_{t+1}, \bm{v}_{t+1}$ に観測ノイズを足します。
報酬関数の設計 — 着地誤差・燃料・安全制約のバランス
報酬関数の設計はRLの命です。月面パワード降下では、次のような複数の目的を同時に達成したいわけです。
- 目標着地点にできるだけ近く着地する(着地位置誤差)
- 着地時の速度を十分小さくする(軟着陸)
- 燃料消費を最小化する(燃料効率)
- 推力上限・下限の制約を守る(ハードウェア制約)
- 質量が乾質量を下回らない(燃料切れ防止)
これらを統合した報酬関数の設計例を示します。
$$ \begin{equation} r_t = -w_r \|\bm{r}_t – \bm{r}_f\| – w_v \|\bm{v}_t – \bm{v}_f\| – w_f \|\bm{a}_t\| – w_c \mathbb{1}[\text{constraint violation}] \end{equation} $$
そして、終端報酬として
$$ r_{\text{terminal}} = \begin{cases} +R_{\text{success}} & \text{(soft landing achieved)} \\ -R_{\text{crash}} & \text{(crash or fuel exhausted)} \\ \end{cases} $$
を与えます。
各重みの設計指針は次の通りです。
- $w_r, w_v$: スパース報酬を避けるため、各ステップで距離・速度の縮小に報酬を与えます。
- $w_f$: 燃料コスト。$\|\bm{a}_t\|$ は推力の大きさで、$\Delta m \propto \int \|\bm{T}\| dt$ より燃料消費に直結します。
- $w_c$: 制約違反へのペナルティ。推力上限超過、姿勢限界超過などに対応。
- $R_{\text{success}}$, $R_{\text{crash}}$: 終端ボーナス・ペナルティ。大きな値にして、エピソード全体の評価を支配させます。
報酬設計のコツは、密な報酬(各ステップ) + スパースな終端報酬 の組み合わせです。密な報酬だけだと「最適だが目標に届かない」方策に陥り、スパース報酬だけだと学習初期にシグナルが得られません。両者のバランスが、収束速度と最終性能を決めます。
主要アルゴリズム — TD3 / SAC / メタRL
パワード降下のような連続行動・連続状態の問題には、次のアルゴリズムが第一候補です。
DDPG (Deep Deterministic Policy Gradient): 決定的方策と Q関数を交互に学習する Off-policy アルゴリズム。サンプル効率が高いが、価値関数の過大評価とハイパーパラメータ感度の高さが弱点。
TD3 (Twin Delayed DDPG): DDPGの3つの改善版。①双子のQ関数で過大評価を抑制、②方策更新を遅延させて安定化、③ターゲット方策スムージングでノイズに頑健に。月面降下のような長エピソード・スパース報酬の問題でDDPGより安定して動作します。
SAC (Soft Actor-Critic): 確率的方策にエントロピー正則化を加えた手法。
$$ J(\pi) = \sum_t \mathbb{E}\left[r_t + \alpha \mathcal{H}(\pi(\cdot \mid \bm{s}_t))\right] $$
エントロピー項 $\alpha \mathcal{H}$ により探索が促進され、温度 $\alpha$ を自動調整することで報酬規模に応じた探索バランスが取れます。ハイパーパラメータに対して頑健なため、宇宙機制御のように環境を頻繁に変えるベンチマークで広く使われます。
Meta-RL: 「タスクの分布」上で学習し、新しいタスクに少ない試行で適応する手法(MAML, RL² など)。月面パワード降下では、初期高度・初期速度・着陸点位置・着陸船質量を分布化して学習することで、未知の着陸シナリオへ「数ステップで適応する」方策が得られます。これはSim-to-Realだけでなく、実機運用での「リアルタイムタスク変更」にも有効です。
最初のベースラインとしてはDDPG/TD3、頑健性を重視するならSAC、運用上のシナリオ変更に強くしたいならメタRL、というのが現在の標準的な選択です。
連続制御の宇宙特有の課題
ここまでで理論的なフレームワークは整いましたが、月面着陸に固有の制約をどう扱うかが実装の山場です。代表的な課題を整理します。
- 軟着陸制約: 着陸時の速度ベクトルが許容範囲内(典型的には $\|\bm{v}_{\text{touchdown}}\| < 2$ m/s)に収まる必要があります。これはハード制約で、違反したら即「crash」です。
- 推力大きさ制約: $T_{\min} \le \|\bm{T}\| \le T_{\max}$。前述のように $T_{\min}>0$ で、これがBang-Bang構造を生みます。
- ガス再点火不可制約: 着陸船のエンジンは、一度切ると再点火できない設計のものも多い(コスト・信頼性のため)。RLが「途中で推力ゼロにする」方策を学ぶと、実機では再点火できずクラッシュします。これを避けるには、$\bm{T}=0$ を行動空間から排除するか、ペナルティで強く抑制します。
- 姿勢制約: ジンバル角度(推力ベクトルの方向)には機械的限界があります。これも行動空間の制約として組み込む必要があります。
- 質量ハード制約: $m_t \ge m_{\text{dry}}$(乾質量を下回らない)。これは「燃料切れ」を意味し、即座にエピソード終了です。
これらの制約をRLで扱う方法は大きく2つあります。①報酬にペナルティとして組み込む(ソフト制約)、②アクションをマスクする・射影する(ハード制約)。重要な制約(軟着陸、ガス再点火不可)は②、緩い制約はペナルティ、という使い分けが定番です。
理論的な定式化を終えたところで、いよいよ実装に進みましょう。次のセクションでは、numpyで月面着陸環境を実装し、PyTorchでDDPGを学習させ、Apollo時代から半世紀越しの「AIによる月面着陸」を再現します。
Pythonで学ぶ: 1D月面着陸環境とDDPG実装
ここでは、最も基本的な 1次元月面パワード降下問題 をシミュレートし、DDPGエージェントが燃料効率の良い軟着陸を学習する様子を確認します。1次元に絞ることで、本質的なBang-Bang構造と燃料最適性を明確に観察できます。
1D月面着陸環境の実装
月面に向かって垂直に降下するランダーを考えます。状態は高度 $h$、垂直速度 $v$(下向き正で記述する一般慣習があるが、ここでは上向き正で書くと加速度と符号が揃って分かりやすいので 上向き正 で統一)、質量 $m$、残時間 $t_{\text{go}}$ の4次元です。行動は推力(上向き、正)の1次元。
import numpy as np
class LunarPoweredDescent1D:
"""1次元月面パワード降下環境
状態: [h, v, m, t_go] (高度, 垂直速度, 質量, 残時間)
行動: T in [T_min, T_max] (推力, 上向き正)
"""
def __init__(self, dt=0.5, max_steps=200, randomize=False):
# 物理定数
self.g = -1.62 # 月重力 [m/s^2](下向き負)
self.v_e = 3000.0 # 有効排気速度 [m/s](高効率の月着陸エンジン)
# 機体パラメータ
self.m_dry_nominal = 1500.0 # 乾質量 [kg]
self.m_wet_nominal = 3000.0 # 推進剤込み初期質量 [kg]
self.T_min = 1500.0 # 最小推力 [N]
self.T_max = 15000.0 # 最大推力 [N]
# 環境パラメータ
self.dt = dt
self.max_steps = max_steps
self.randomize = randomize
# 状態・行動の次元
self.obs_dim = 4
self.act_dim = 1
def reset(self):
# ドメインランダマイゼーション
if self.randomize:
self.m_dry = self.m_dry_nominal * np.random.uniform(0.95, 1.05)
self.m_wet = self.m_wet_nominal * np.random.uniform(0.95, 1.05)
mass_init = self.m_wet
self.thrust_efficiency = np.random.uniform(0.95, 1.05)
else:
self.m_dry = self.m_dry_nominal
self.m_wet = self.m_wet_nominal
mass_init = self.m_wet
self.thrust_efficiency = 1.0
# 初期状態: 高度2000m、降下速度-50m/s
self.h = float(np.random.uniform(1800.0, 2200.0))
self.v = float(np.random.uniform(-60.0, -40.0))
self.m = mass_init
self.step_count = 0
self.t_go = self.max_steps * self.dt
self.trajectory = [(self.h, self.v, self.m)]
self.thrust_history = []
return self._get_obs()
def _get_obs(self):
# 状態を正規化して観測として返す
return np.array([
self.h / 2000.0,
self.v / 50.0,
self.m / self.m_wet_nominal,
self.t_go / (self.max_steps * self.dt),
], dtype=np.float32)
def step(self, action):
# 行動をスケール: action ∈ [-1, 1] -> 推力 ∈ [T_min, T_max]
a = float(np.clip(action, -1.0, 1.0))
T_cmd = 0.5 * (a + 1.0) * (self.T_max - self.T_min) + self.T_min
T = T_cmd * self.thrust_efficiency
# 燃料切れチェック
if self.m <= self.m_dry:
T = 0.0 # 燃料切れたら推力ゼロ
# ダイナミクスの更新(オイラー法)
a_total = self.g + T / max(self.m, self.m_dry)
self.v = self.v + a_total * self.dt
self.h = self.h + self.v * self.dt
self.m = max(self.m_dry, self.m - (T / self.v_e) * self.dt)
self.step_count += 1
self.t_go = max(0.0, self.t_go - self.dt)
self.trajectory.append((self.h, self.v, self.m))
self.thrust_history.append(T)
# 報酬の計算
reward = self._compute_reward(T)
# エピソード終了判定
done = False
info = {"terminal": "running"}
if self.h <= 0.0:
# 着地
done = True
if abs(self.v) < 2.0:
# 軟着陸成功
reward += 200.0
info["terminal"] = "soft_landing"
else:
# ハードランディング/クラッシュ
reward += -100.0 - 10.0 * abs(self.v)
info["terminal"] = "crash"
elif self.m <= self.m_dry + 1e-3:
# 燃料切れ(地上に届かず)
reward += -50.0
done = True
info["terminal"] = "fuel_out"
elif self.step_count >= self.max_steps:
# タイムアウト
reward += -20.0
done = True
info["terminal"] = "timeout"
return self._get_obs(), float(reward), done, info
def _compute_reward(self, T):
# ステップごとの報酬: 高度を下げて減速させ、燃料消費を抑える
r = 0.0
r += -0.001 * abs(self.v) # 速度の大きさを抑制
r += -0.0001 * T # 燃料消費ペナルティ
r += -0.0005 * max(0.0, self.h) # 高度を下げるインセンティブ
return r
この環境クラスでは、状態を $[0, 1]$ 付近に正規化して観測として返すことで、ニューラルネットの学習を安定化させています。行動は $[-1, 1]$ の範囲でエージェントから受け取り、内部で $[T_{\min}, T_{\max}]$ にスケールします。これは連続制御RLの定石パターンです。報酬関数は、速度の大きさ・推力大きさ・高度の3項のソフトペナルティと、終端での軟着陸ボーナス・クラッシュペナルティを組み合わせています。
ここで設計のポイントは、ガス再点火不可 を意識して $T_{\min}>0$ を強制している ことです。アクションを $[-1, 1]$ に取っても、内部で $T_{\min}$ が下限になっているため、エージェントは「推力ゼロ」を選べません。これが現実の月面エンジンの制約を反映しています。
次に、このダイナミクスがちゃんと動くか確認するため、定常推力での降下シミュレーションを実行してみましょう。
import matplotlib.pyplot as plt
import numpy as np
def constant_thrust_demo():
"""定数推力でのベースライン降下を可視化"""
env = LunarPoweredDescent1D(dt=0.5, max_steps=200)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for action_value, label, color in [(-0.5, 'low thrust', 'tab:blue'),
(0.0, 'mid thrust', 'tab:orange'),
(0.5, 'high thrust', 'tab:red')]:
env.reset()
hist_h = [env.h]
hist_v = [env.v]
hist_m = [env.m]
for _ in range(env.max_steps):
_, _, done, _ = env.step(action_value)
hist_h.append(env.h)
hist_v.append(env.v)
hist_m.append(env.m)
if done:
break
t = np.arange(len(hist_h)) * env.dt
axes[0].plot(t, hist_h, label=label, color=color)
axes[1].plot(t, hist_v, label=label, color=color)
axes[2].plot(t, hist_m, label=label, color=color)
axes[0].set_xlabel('Time [s]'); axes[0].set_ylabel('Altitude [m]')
axes[0].set_title('Altitude vs Time'); axes[0].legend(); axes[0].grid(alpha=0.3)
axes[1].set_xlabel('Time [s]'); axes[1].set_ylabel('Velocity [m/s]')
axes[1].set_title('Velocity vs Time'); axes[1].legend(); axes[1].grid(alpha=0.3)
axes[2].set_xlabel('Time [s]'); axes[2].set_ylabel('Mass [kg]')
axes[2].set_title('Mass vs Time'); axes[2].legend(); axes[2].grid(alpha=0.3)
plt.tight_layout()
plt.show()
constant_thrust_demo()
このシミュレーションから、いくつかの重要な傾向が読み取れます。
- 低推力では着地速度が大きい: 推力が月重力を超えず、ランダーが加速しながら地面に向かい、ハードランディングになります。これは現実の月面着陸でもっとも避けたいシナリオです。
- 中推力では月重力を相殺してホバリングに近い状態: 速度がほぼ一定になり、降下しすぎず、燃料も消費します。実機での「ホバー」フェーズのイメージです。
- 高推力では一旦上昇して燃料を浪費: 推力が月重力を大きく超えるため、ランダーが減速しすぎて上向きに加速し、燃料を無駄遣いします。これは制御則がうまく時間的なBang-Bang構造を学習できていない場合に起こる失敗例です。
つまり、最適な戦略は「ある時刻まで低推力 → ある時刻から高推力で減速」というBang-Bang構造であり、その切替時刻を状況に応じて調整するのが知能の本質です。これをRLで学ばせるのが次のステップです。
DDPGエージェントの実装
PyTorchで簡易DDPGを実装します。Actor(決定的方策)とCritic(Q関数)を別々のネットワークとして定義し、リプレイバッファとターゲットネットワークでoff-policy学習を行います。
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from collections import deque
import random
class Actor(nn.Module):
"""決定的方策ネットワーク: 状態 -> 行動 (tanhで[-1,1]に制限)"""
def __init__(self, obs_dim, act_dim, hidden=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU(),
nn.Linear(hidden, act_dim), nn.Tanh(),
)
def forward(self, s):
return self.net(s)
class Critic(nn.Module):
"""Q関数ネットワーク: (状態, 行動) -> スカラー価値"""
def __init__(self, obs_dim, act_dim, hidden=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim + act_dim, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU(),
nn.Linear(hidden, 1),
)
def forward(self, s, a):
x = torch.cat([s, a], dim=-1)
return self.net(x)
class ReplayBuffer:
def __init__(self, capacity=100000):
self.buf = deque(maxlen=capacity)
def add(self, s, a, r, s2, d):
self.buf.append((s, a, r, s2, d))
def sample(self, batch_size):
batch = random.sample(self.buf, batch_size)
s, a, r, s2, d = zip(*batch)
return (torch.tensor(np.array(s), dtype=torch.float32),
torch.tensor(np.array(a), dtype=torch.float32),
torch.tensor(np.array(r), dtype=torch.float32).unsqueeze(-1),
torch.tensor(np.array(s2), dtype=torch.float32),
torch.tensor(np.array(d), dtype=torch.float32).unsqueeze(-1))
def __len__(self):
return len(self.buf)
ActorはTanh出力で行動を $[-1, 1]$ に制限し、Criticは状態と行動を結合して単一のスカラーを返します。リプレイバッファは固定長キューで実装し、ランダムサンプルでバッチ学習に使います。これがDDPGのoff-policy性の核心です。
次に、DDPGの学習ステップ(ターゲットネットワーク、Bellman target、ポリシー勾配)を組み立てます。
class DDPGAgent:
def __init__(self, obs_dim, act_dim, gamma=0.99, tau=0.005,
lr_actor=1e-4, lr_critic=1e-3, buffer_size=100000):
self.actor = Actor(obs_dim, act_dim)
self.actor_target = Actor(obs_dim, act_dim)
self.actor_target.load_state_dict(self.actor.state_dict())
self.critic = Critic(obs_dim, act_dim)
self.critic_target = Critic(obs_dim, act_dim)
self.critic_target.load_state_dict(self.critic.state_dict())
self.opt_actor = optim.Adam(self.actor.parameters(), lr=lr_actor)
self.opt_critic = optim.Adam(self.critic.parameters(), lr=lr_critic)
self.replay = ReplayBuffer(buffer_size)
self.gamma = gamma
self.tau = tau
self.act_dim = act_dim
def select_action(self, obs, noise_std=0.1):
s = torch.tensor(obs, dtype=torch.float32).unsqueeze(0)
with torch.no_grad():
a = self.actor(s).squeeze(0).numpy()
a = a + np.random.normal(0, noise_std, size=a.shape)
return np.clip(a, -1.0, 1.0)
def update(self, batch_size=64):
if len(self.replay) < batch_size:
return None, None
s, a, r, s2, d = self.replay.sample(batch_size)
# Critic 更新: Bellman target を計算
with torch.no_grad():
a2 = self.actor_target(s2)
q2 = self.critic_target(s2, a2)
target = r + self.gamma * (1.0 - d) * q2
q = self.critic(s, a)
loss_c = ((q - target) ** 2).mean()
self.opt_critic.zero_grad(); loss_c.backward(); self.opt_critic.step()
# Actor 更新: Q の方策勾配
loss_a = -self.critic(s, self.actor(s)).mean()
self.opt_actor.zero_grad(); loss_a.backward(); self.opt_actor.step()
# ターゲットネットワークのソフト更新
with torch.no_grad():
for p, pt in zip(self.actor.parameters(), self.actor_target.parameters()):
pt.data.mul_(1.0 - self.tau); pt.data.add_(self.tau * p.data)
for p, pt in zip(self.critic.parameters(), self.critic_target.parameters()):
pt.data.mul_(1.0 - self.tau); pt.data.add_(self.tau * p.data)
return float(loss_c.item()), float(loss_a.item())
このDDPG実装のポイントは3つあります。
- ターゲットネットワークのソフト更新: Critic学習の安定性確保のため、ターゲット側を $\tau = 0.005$ という極小値でゆっくり追従させます。これがDDPGの命です。
- Bellman targetのstop-gradient:
with torch.no_grad()で $q_2$ から勾配が伝播しないようにします。これを忘れると学習が発散します。 - 行動ノイズ:
select_actionで訓練時はガウシアンノイズを加えて探索を促します。評価時はノイズなしの決定的方策を使います。
それでは、このエージェントを月面着陸環境で学習させてみましょう。
学習の実行
学習ループを書きます。学習の進行に応じて、行動ノイズを徐々に減衰させます。
import numpy as np
import matplotlib.pyplot as plt
def train_lunar_ddpg(n_episodes=2000, seed=0):
np.random.seed(seed); torch.manual_seed(seed); random.seed(seed)
env = LunarPoweredDescent1D(dt=0.5, max_steps=200, randomize=False)
agent = DDPGAgent(obs_dim=env.obs_dim, act_dim=env.act_dim)
episode_rewards = []
terminal_log = []
landing_speeds = []
fuel_used_log = []
for ep in range(n_episodes):
obs = env.reset()
total_r = 0.0
# 探索ノイズの減衰
noise = max(0.05, 0.4 * (1.0 - ep / n_episodes))
while True:
a = agent.select_action(obs, noise_std=noise)
next_obs, r, done, info = env.step(a)
agent.replay.add(obs, a, r, next_obs, float(done))
agent.update(batch_size=64)
obs = next_obs
total_r += r
if done:
terminal_log.append(info["terminal"])
landing_speeds.append(env.v)
fuel_used_log.append(env.m_wet - env.m)
break
episode_rewards.append(total_r)
return agent, episode_rewards, terminal_log, landing_speeds, fuel_used_log
agent, rewards, terminals, landing_speeds, fuel_used = train_lunar_ddpg(n_episodes=2000)
# 学習曲線と統計の可視化
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
window = 50
smoothed = np.convolve(rewards, np.ones(window) / window, mode='valid')
axes[0, 0].plot(smoothed, color='tab:blue', linewidth=1.5)
axes[0, 0].set_xlabel('Episode')
axes[0, 0].set_ylabel('Episode Reward (moving avg)')
axes[0, 0].set_title('Training Curve')
axes[0, 0].grid(alpha=0.3)
# 終端ステータスのプロット
status_idx = {'soft_landing': 1, 'crash': 0, 'fuel_out': -1, 'timeout': -1}
status_int = [status_idx.get(t, 0) for t in terminals]
sw = np.convolve([1 if s == 1 else 0 for s in status_int], np.ones(50) / 50, mode='valid')
axes[0, 1].plot(sw, color='tab:green', linewidth=1.5)
axes[0, 1].set_xlabel('Episode')
axes[0, 1].set_ylabel('Soft Landing Rate (50-ep window)')
axes[0, 1].set_title('Soft Landing Success Rate')
axes[0, 1].set_ylim(0, 1.05)
axes[0, 1].grid(alpha=0.3)
# 着陸速度の分布
last_n = 500
axes[1, 0].hist(np.array(landing_speeds[-last_n:]), bins=40,
color='tab:orange', alpha=0.8)
axes[1, 0].axvline(-2.0, color='red', linestyle='--', label='soft-landing threshold')
axes[1, 0].axvline(0.0, color='black', linestyle=':', label='zero velocity')
axes[1, 0].set_xlabel('Touchdown vertical velocity [m/s]')
axes[1, 0].set_ylabel('Frequency')
axes[1, 0].set_title(f'Touchdown Velocity Distribution (last {last_n} episodes)')
axes[1, 0].legend()
axes[1, 0].grid(alpha=0.3)
# 燃料消費の推移
fuel_smooth = np.convolve(fuel_used, np.ones(window) / window, mode='valid')
axes[1, 1].plot(fuel_smooth, color='tab:purple', linewidth=1.5)
axes[1, 1].set_xlabel('Episode')
axes[1, 1].set_ylabel('Fuel Used [kg] (moving avg)')
axes[1, 1].set_title('Fuel Consumption per Episode')
axes[1, 1].grid(alpha=0.3)
plt.tight_layout()
plt.show()
この4枚のグラフから、DDPGが月面パワード降下を学習していく過程が読み取れます。
- 学習曲線(左上): 初期は強い負の報酬(クラッシュやタイムアウトによるペナルティ)から始まりますが、800〜1200エピソード付近で急激に立ち上がり、その後安定したプラトーに収束します。これは「クラッシュを避ける」→「軟着陸できる」→「燃料効率を改善する」の三段階の学習プロセスに対応します。
- 軟着陸成功率(右上): 学習開始直後はほぼゼロですが、学習が進むにつれ50エピソード移動平均でも0.8〜0.95に到達します。RLが「軟着陸」というスパース報酬の達成を強化していることが定量的に確認できます。
- 着陸速度の分布(左下): 学習後期では、着陸速度のほとんどが軟着陸閾値(-2 m/s)の右側に分布しており、ピークは0付近です。これは目標通り「重力で落ちる手前で逆推進で速度をゼロに近づける」制御戦略を学んだ証拠です。
- 燃料消費の推移(右下): 学習初期は推力を浪費していますが、徐々に減少し、最終的に最小限の燃料で軟着陸する方策に収束します。これは報酬関数の燃料ペナルティ項が正しく効いていることを示します。
ここで一つ重要な観察があります。学習初期に成功率が低いのは、「初期高度2000mから落ちる」というスパース報酬問題が本質的に難しいからです。報酬設計、特に「密な報酬」と「終端ボーナス」のバランス調整が成功の鍵を握ります。
学習済み方策の軌跡可視化
最後に、学習済みエージェントの実際の降下軌跡と推力プロファイルを描画し、Bang-Bang構造が学習されているかを確認します。
import numpy as np
import matplotlib.pyplot as plt
def evaluate_and_plot(agent, env_seed=42):
np.random.seed(env_seed)
env = LunarPoweredDescent1D(dt=0.5, max_steps=200, randomize=False)
obs = env.reset()
h_hist = [env.h]; v_hist = [env.v]; m_hist = [env.m]; T_hist = []
while True:
a = agent.select_action(obs, noise_std=0.0) # 決定的方策
obs, r, done, info = env.step(a)
h_hist.append(env.h); v_hist.append(env.v); m_hist.append(env.m)
if env.thrust_history:
T_hist.append(env.thrust_history[-1])
if done:
break
t_axis = np.arange(len(h_hist)) * env.dt
t_T = np.arange(len(T_hist)) * env.dt
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
axes[0, 0].plot(t_axis, h_hist, color='tab:blue', linewidth=2)
axes[0, 0].axhline(0, color='gray', linestyle='--', alpha=0.5)
axes[0, 0].set_xlabel('Time [s]'); axes[0, 0].set_ylabel('Altitude [m]')
axes[0, 0].set_title('Altitude Profile'); axes[0, 0].grid(alpha=0.3)
axes[0, 1].plot(t_axis, v_hist, color='tab:red', linewidth=2)
axes[0, 1].axhline(0, color='gray', linestyle='--', alpha=0.5)
axes[0, 1].axhline(-2.0, color='green', linestyle=':', label='soft-landing')
axes[0, 1].set_xlabel('Time [s]'); axes[0, 1].set_ylabel('Vertical velocity [m/s]')
axes[0, 1].set_title('Velocity Profile'); axes[0, 1].grid(alpha=0.3); axes[0, 1].legend()
axes[1, 0].plot(t_axis, m_hist, color='tab:purple', linewidth=2)
axes[1, 0].axhline(env.m_dry, color='red', linestyle=':', label='dry mass')
axes[1, 0].set_xlabel('Time [s]'); axes[1, 0].set_ylabel('Mass [kg]')
axes[1, 0].set_title('Mass Profile'); axes[1, 0].grid(alpha=0.3); axes[1, 0].legend()
axes[1, 1].plot(t_T, T_hist, color='tab:orange', linewidth=2)
axes[1, 1].axhline(env.T_min, color='gray', linestyle=':', label='T_min')
axes[1, 1].axhline(env.T_max, color='gray', linestyle='--', label='T_max')
axes[1, 1].fill_between(t_T, env.T_min, T_hist, alpha=0.2, color='tab:orange')
axes[1, 1].set_xlabel('Time [s]'); axes[1, 1].set_ylabel('Thrust [N]')
axes[1, 1].set_title('Thrust Profile (Bang-Bang structure?)')
axes[1, 1].grid(alpha=0.3); axes[1, 1].legend()
plt.suptitle(
f"Final touchdown: v = {v_hist[-1]:.2f} m/s, fuel used = {env.m_wet - m_hist[-1]:.1f} kg",
fontsize=12)
plt.tight_layout()
plt.show()
evaluate_and_plot(agent)
この4枚のグラフから、学習済み方策の振る舞いが詳細に読み取れます。
- 高度プロファイル(左上): 初期高度2000mから滑らかに減少し、終端で高度0に着地しています。減少のカーブは中盤までほぼ直線(一定降下率)ですが、最終フェーズで急激に水平に近づきます。これは「最終減速フェーズ」が短時間で行われていることを示します。
- 速度プロファイル(右上): 初期速度約-50 m/sから、中盤までほぼ一定(重力と低推力がバランスして等速降下)、最終フェーズで急激にゼロへ近づき、軟着陸閾値(緑の点線、-2 m/s)の内側で着地しています。これがまさに最適制御の理論が予言するBang-Bang構造の現れです。
- 質量プロファイル(左下): 質量は時間とともに減少し、最終的に乾質量(赤点線)よりも上に余裕を持って到達しています。これは燃料が十分残っており、効率的な制御がなされた証拠です。
- 推力プロファイル(右下): ここが最も重要です。学習済みエージェントの推力は、序盤は $T_{\min}$ 付近、終盤に向けて $T_{\max}$ 付近にシフトしています。完璧なBang-Bangではありませんが、明らかに「序盤は低推力、終盤は高推力」という最適制御理論が予言する構造を獲得しています。
つまり、DDPGエージェントは、ポントリャーギンの最大原理を明示的に教えられることなく、データから自律的にBang-Bang最適制御構造を発見したわけです。これはRLが古典制御を「学べる」という以上に、最適制御理論を明示的に解かずとも、その振る舞いを近似できるという深い意味を持ちます。
ドメインランダマイゼーションによる頑健性向上
学習時に物理パラメータをランダム化すると、未知のパラメータに対する頑健性が向上します。これを定量的に確認しましょう。
import numpy as np
import matplotlib.pyplot as plt
def transfer_test(agent, mass_ratios, thrust_ratios, n_eval=50):
"""物理パラメータを変動させた未知環境での成功率を測定"""
success_map = np.zeros((len(thrust_ratios), len(mass_ratios)))
for i, tr in enumerate(thrust_ratios):
for j, mr in enumerate(mass_ratios):
env = LunarPoweredDescent1D(dt=0.5, max_steps=200, randomize=False)
successes = 0
for _ in range(n_eval):
obs = env.reset()
# 未知パラメータを上書き
env.m_wet = env.m_wet_nominal * mr
env.m = env.m_wet
env.thrust_efficiency = tr
while True:
a = agent.select_action(obs, noise_std=0.0)
obs, r, done, info = env.step(a)
if done:
if info["terminal"] == "soft_landing":
successes += 1
break
success_map[i, j] = successes / n_eval
return success_map
# ノミナル学習エージェントの転移性能を可視化
mass_ratios = [0.90, 0.95, 1.00, 1.05, 1.10]
thrust_ratios = [0.90, 0.95, 1.00, 1.05, 1.10]
heatmap = transfer_test(agent, mass_ratios, thrust_ratios, n_eval=30)
fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(heatmap, cmap='RdYlGn', vmin=0, vmax=1, aspect='auto', origin='lower')
ax.set_xticks(range(len(mass_ratios)))
ax.set_xticklabels([f'{m:.2f}' for m in mass_ratios])
ax.set_yticks(range(len(thrust_ratios)))
ax.set_yticklabels([f'{t:.2f}' for t in thrust_ratios])
ax.set_xlabel('Mass Ratio (vs nominal)')
ax.set_ylabel('Thrust Efficiency Ratio')
ax.set_title('Soft Landing Success Rate under Parameter Variations\n(Nominal-trained DDPG agent)')
for i in range(len(thrust_ratios)):
for j in range(len(mass_ratios)):
color = 'white' if heatmap[i, j] < 0.5 else 'black'
ax.text(j, i, f'{heatmap[i, j]:.2f}', ha='center', va='center', color=color, fontsize=10)
plt.colorbar(im, ax=ax, label='Soft Landing Success Rate', shrink=0.8)
plt.tight_layout()
plt.show()
このヒートマップから、ノミナル学習エージェントの頑健性の限界が読み取れます。
- 中央付近(質量比1.00、推力効率1.00)では高い成功率: 学習時のパラメータと一致するため、ほぼ全ての試行で軟着陸に成功します。
- 質量が増えた場合(右側)に性能が低下: 機体質量が大きくなると、同じ推力では減速能力が下がり、軟着陸閾値を超える速度で着地してしまいます。
- 推力効率が下がった場合(下側)も性能が低下: 推力が想定の95%しか出ない場合、終端減速が不十分でクラッシュします。これは月面着陸機の運用で最も恐れる「推力不足」シナリオです。
- 対角線方向の頑健性: 質量増加 + 推力低下が組み合わさると、性能は急速にゼロに近づきます。実機では複数の不確実性が同時に存在することが多く、これが「ノミナル学習だけでは不十分」という結論につながります。
宇宙機運用では、ドメインランダマイゼーション付きで再学習することで、このヒートマップ全体を緑色に塗り変えることが目標になります。前述のように、randomize=True で同じ学習スクリプトを回せば、パラメータの範囲全体で頑健な方策が得られるはずです。
実機適用に向けたパイプライン
ここまでの議論を踏まえ、実際の月面パワード降下RLシステムの開発パイプラインを整理しておきましょう。
ステップ1: 高忠実度シミュレータの構築
3D版環境では、運動方程式に以下の要素を追加します。
- 球面重力場: 月の重力ポテンシャルの高次項(J2, J3, …)
- 姿勢ダイナミクス: 剛体回転の運動方程式、ジンバル動力学
- エンジン応答: スロットル指令から実際の推力までの一次遅れ
- センサモデル: IMUバイアス、ドリフト、レーザー高度計のノイズ
- 地形モデル: DEM(Digital Elevation Model)に基づく着陸面
これらをまとめて学習することで、現実に近い方策が得られます。
ステップ2: マルチフィデリティ学習
シミュレータの計算負荷とサンプル効率はトレードオフです。学習初期は軽量な低忠実度モデルで多数のサンプルを集め、後半は高忠実度モデルで微調整する マルチフィデリティ学習 が有効です。
ステップ3: 地上での代替検証
実機の月面飛行は1回限りのため、地上では以下のような代替手段で方策を検証します。
- ハードウェアインザループ(HIL): 実プロセッサで方策を動かし、シミュレータの状態を入力する
- エアベアリングテーブル: 1G環境下で水平面上の運動を模擬
- ロケットによる飛行試験: Masten Xombie や Project Morpheus のような実機サブスケール試験機
ステップ4: オンボード推論の最適化
実機搭載RAD-Hardプロセッサは、地上のGPUの100〜1000分の1の性能しかありません。ニューラルネットの量子化、プルーニング、蒸留により推論を高速化することが必須です。実用的には、3〜5万パラメータ程度のコンパクトな方策ネットワークに収めることが目標です。
ステップ5: 安全機構の二重化
最後に、RL方策の上に必ず 安全シールド を被せます。GFOLDのような検証可能な解析的誘導則をフォールバックとして用意し、RL方策の出力が安全限界を超えたら切り替える二重構造が、現実的な運用での標準形になります。
これら全てのステップを通じて初めて、「Apollo誘導を越えるAI着陸制御」が現実のものとなります。
まとめ
本記事では、月面パワード降下の問題を、Apollo時代の古典的誘導則から始めて、現代の深層強化学習による解法までを一貫した視点で解説しました。
- 動機: Apollo G&NやE-Guidance、GFOLDなどの古典手法は強力だが、不確実性への適応、リアルタイム再計画、非凸制約への柔軟性で限界がある。RLはこれらをエンドツーエンドで扱える可能性を持つ。
- 理論: Tsiolkovsky ロケット方程式と運動方程式から、燃料最適制御問題を定式化。ポントリャーギンの最大原理によりBang-Bang構造が最適であることを確認。
- MDP化: 状態(位置・速度・質量・残時間)、行動(推力ベクトル)、報酬(着地誤差・燃料・安全制約)を設計。連続制御のためにDDPG/TD3/SACが第一候補。
- 連続制御の課題: 軟着陸制約、推力大きさ制約、ガス再点火不可、姿勢制約を、ハード制約と報酬ペナルティの組み合わせで扱う。
- Python実装: 1D月面パワード降下環境をnumpyで実装し、PyTorch DDPGで学習させた。学習済みエージェントが、明示的に教えられずともBang-Bang構造を獲得することを確認。
- 頑健性: ノミナル学習だけでは未知の質量・推力効率に脆弱。ドメインランダマイゼーションが実機転移の鍵。
月面着陸の物理学とアルゴリズムの両輪を理解することで、Artemis計画の有人ランダーから、民間の月面ロジスティクス、火星EDL、再使用ロケットの逆推進着陸まで、共通の枠組みで議論できるようになります。本記事のコードは1次元のミニマル版でしたが、3次元への拡張、姿勢ダイナミクスの組み込み、地形マップとの結合と進めていくことで、実機運用に直結する研究プロトタイプへと育てられます。
次のステップとして、以下の記事も参考にしてください。