宇宙ロボットの信頼性とフォールトトレランス — 壊れても動き続ける設計

国際宇宙ステーション(ISS)のロボットアーム「カナダアーム2」が関節の1つを失ったとき、ミッション全体が停止するでしょうか? 地上であれば修理工を呼べばよいのですが、軌道上ではそうはいきません。部品交換のためだけに宇宙飛行士の船外活動(EVA)を実施するコストは1回あたり数千万ドルに達し、そもそもスケジュール調整だけで数か月を要します。火星探査ローバーに至っては、修理に向かうこと自体が不可能です。

つまり宇宙ロボットには「壊れないこと」だけでなく、「壊れても動き続けること」 が求められます。この設計思想を フォールトトレランス(Fault Tolerance) と呼びます。

フォールトトレランスの考え方は、宇宙ロボットにとどまらず幅広い工学分野で活用されています。

  • 航空宇宙: 航空機のフライ・バイ・ワイヤシステムにおける冗長コンピュータ
  • 原子力プラント: 安全系の多重化による事故防止
  • 自動運転車: センサやアクチュエータの冗長配置
  • データセンター: RAID構成やレプリケーションによるデータ保護

本記事の内容

  • 宇宙ロボットに信頼性が極めて重要な理由
  • 宇宙環境特有の故障要因(放射線、温度サイクル、デブリなど)
  • 冗長設計の数学的基盤と関節冗長性の活用
  • 故障検知・隔離・回復(FDIR)の体系的手法
  • 故障後の運動学的再構成とヤコビ行列の再計算
  • フォールトトレラント制御の理論
  • ソフトウェアフォールトトレランス
  • Pythonによる関節故障時の可操作度低下シミュレーション

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

宇宙ロボットに信頼性が不可欠な理由

「信頼性」というと漠然とした印象を受けるかもしれません。しかし宇宙ロボットの文脈では、信頼性の低さが直接的に ミッション失敗人命リスク につながるという、地上とは根本的に異なる事情があります。ここでは、なぜ宇宙ロボットに「壊れても動く」設計が不可欠なのかを整理します。

修理が極めて困難

地球上のロボットであれば、故障した部品を交換するだけで復旧できます。しかし軌道上では、交換部品を届けるためだけに補給ミッションを手配する必要があり、数か月から数年の遅延が生じます。深宇宙ミッション(火星探査ローバーなど)では、物理的に修理が不可能です。したがって、故障が起きても自律的に動作を継続できる能力 がシステム設計の段階から組み込まれなければなりません。

ミッション全体への波及

宇宙ロボットは通常、大型のミッションの一部として機能しています。例えば、ISS上のロボットアームが故障すると、船外機器の移設作業が停止し、他の実験スケジュールや補給船のドッキング計画にも影響が連鎖します。1つのコンポーネントの故障がミッション全体のスケジュールと予算に波及する — これが宇宙ミッションの特徴です。

信頼性の定量化

信頼性を数学的に扱うために、信頼度関数 $R(t)$ を定義します。これは、時刻 $t$ までシステムが故障せずに動作し続ける確率です。

直感的には「時間が経てば壊れやすくなる」という当たり前の感覚を数式で表現したものです。最も基本的なモデルとして、故障率 $\lambda$ が一定の場合(偶発故障期間)を考えると:

$$ R(t) = e^{-\lambda t} $$

ここで $\lambda$ は単位時間あたりの故障率(故障/時間)です。この指数関数モデルから、平均故障間隔(MTBF: Mean Time Between Failures) が計算できます:

$$ \text{MTBF} = \int_0^{\infty} R(t) \, dt = \int_0^{\infty} e^{-\lambda t} \, dt = \frac{1}{\lambda} $$

この積分では $e^{-\lambda t}$ を $0$ から $\infty$ まで積分しています。$\left[-\frac{1}{\lambda}e^{-\lambda t}\right]_0^{\infty} = 0 – \left(-\frac{1}{\lambda}\right) = \frac{1}{\lambda}$ と求まります。

例えば、ある関節アクチュエータの故障率が $\lambda = 10^{-5}$ /時間であれば、MTBF は $10^5$ 時間(約11.4年)です。一見十分に思えますが、7自由度ロボットアームには7つの関節があり、いずれか1つが故障する確率は無視できなくなります。

直列システムの脆弱性

$n$ 個のコンポーネントが 直列 に構成されている場合(1つでも壊れるとシステム全体が停止する場合)、システム全体の信頼度は各コンポーネントの信頼度の積になります:

$$ R_{\text{sys}}(t) = \prod_{i=1}^{n} R_i(t) $$

各コンポーネントが同一の故障率 $\lambda$ を持つとすると:

$$ R_{\text{sys}}(t) = \left(e^{-\lambda t}\right)^n = e^{-n\lambda t} $$

つまり、システム全体の故障率は $n\lambda$ に増大します。7自由度ロボットアームなら、各関節のMTBFが11.4年でも、システム全体のMTBFは $\frac{11.4}{7} \approx 1.6$ 年に縮まります。10年以上のミッション寿命が要求される宇宙ロボットにとって、これは到底受け入れられない数字です。

この「直列システムの脆弱性」こそが、冗長設計やフォールトトレランスが不可欠な理由です。次のセクションでは、そもそもどのような故障が宇宙環境で発生するのかを整理します。

宇宙環境の故障要因

地上の工場ロボットと宇宙ロボットでは、直面する故障モードが根本的に異なります。地上では摩耗や汚染が主な原因ですが、宇宙では 地上では存在しない過酷な環境要因 がロボットの各コンポーネントを攻撃します。ここでは主要な故障要因を整理します。

放射線による半導体損傷

宇宙空間には、太陽フレアからの高エネルギー粒子や銀河宇宙線が飛び交っています。これらの放射線がロボットの電子回路に与える影響は大きく2つあります。

シングルイベントアップセット(SEU): 高エネルギー粒子がメモリセルに衝突し、ビットを反転させる現象です。ソフトウェアの誤動作や制御パラメータの破損を引き起こします。SEUの発生率はフラックス $\Phi$(粒子/cm$^2$/s)とデバイスの感受断面積 $\sigma$(cm$^2$)の積で近似されます:

$$ R_{\text{SEU}} = \Phi \cdot \sigma $$

トータルイオナイジングドーズ(TID): 長期間にわたる累積被ばくにより、半導体の閾値電圧がシフトし、最終的にデバイスが機能停止します。低軌道(LEO)でも年間数krad程度の被ばくが蓄積されるため、10年ミッションでは数十krad以上の耐放射線性が必要です。

熱サイクルによる疲労

軌道上のロボットは、太陽光が当たる面は $+120°$C以上、影の面は $-150°$C以下という極端な温度差にさらされます。LEOでは約90分周期で昼と夜が切り替わるため、1日に16回もの熱サイクルが発生します。10年間では約 $16 \times 365 \times 10 \approx 58{,}400$ 回の熱サイクルとなり、金属材料の熱疲労はんだ接合部のクラック を引き起こします。

熱応力 $\sigma_{\text{th}}$ は、線膨張係数 $\alpha$ の異なる2つの材料が接合されている場合に生じます:

$$ \sigma_{\text{th}} = E \cdot \Delta\alpha \cdot \Delta T $$

ここで $E$ はヤング率、$\Delta\alpha$ は線膨張係数の差、$\Delta T$ は温度変化幅です。異種金属の接合部(例えばアルミニウム構造体にステンレスの軸受を組み込んだ関節)では、この熱応力が繰り返し作用し、クラックが進展します。

スペースデブリの衝突

直径1mm以下の微小デブリでも、相対速度が 7〜15 km/s に達するため、衝突時のエネルギーは膨大です。運動エネルギーは:

$$ E_k = \frac{1}{2} m v^2 $$

質量 $m = 10^{-3}$ g、速度 $v = 10$ km/s のデブリでも、$E_k = \frac{1}{2} \times 10^{-6} \times (10^4)^2 = 50$ J となり、これは小口径銃弾のエネルギーに匹敵します。ロボットアームのリンクやケーブルハーネスに衝突すれば、局所的な貫通や断線が生じます。

潤滑剤の蒸発と摩耗

地上の機械関節で使われるグリースやオイルは、宇宙の高真空環境($10^{-7}$ Pa以下)では急速に蒸発します。このため、宇宙ロボットの関節には 固体潤滑剤(二硫化モリブデン MoS$_2$ やダイヤモンドライクカーボン DLC)が使用されますが、長期運用では摩耗が進行し、トルクの増大や関節のロックを引き起こします。

故障要因のまとめ

故障要因 影響を受けるコンポーネント 典型的な故障モード
放射線(SEU) 制御コンピュータ、センサ ビット反転、誤動作
放射線(TID) 半導体デバイス全般 閾値シフト、機能停止
熱サイクル 構造体、はんだ接合、ケーブル 疲労クラック、断線
デブリ衝突 リンク、ケーブルハーネス 貫通、断線
真空環境 関節軸受、歯車 潤滑不良、ロック

これらの故障要因に対処するために、宇宙ロボットの設計では 冗長性 を積極的に導入します。次のセクションでは、冗長設計の数学的基盤を見ていきます。

冗長設計 — 故障に備える余裕を持つ

日常生活で「予備」を持つことの重要性は誰もが知っています。旅行に出かけるときに予備のバッテリーを持っていく、車にスペアタイヤを積んでおく — 冗長設計はこの発想を工学的に体系化したものです。宇宙ロボットの場合、最も重要な冗長性は 関節の冗長性 です。

並列冗長によるシステム信頼度の向上

前のセクションで見た直列システムの脆弱性を解消するために、並列冗長 を導入します。同一機能を持つコンポーネントを $k$ 個並列に配置し、少なくとも1つが動作していればシステムが機能するようにします。

$k$ 個の並列コンポーネントが全て故障する確率は:

$$ \prod_{i=1}^{k} (1 – R_i(t)) $$

したがって、少なくとも1つが生存している確率(並列システムの信頼度)は:

$$ R_{\text{parallel}}(t) = 1 – \prod_{i=1}^{k} (1 – R_i(t)) $$

各コンポーネントが同じ信頼度 $R(t)$ を持つ場合:

$$ R_{\text{parallel}}(t) = 1 – (1 – R(t))^k $$

例えば、$R(t) = 0.9$ のコンポーネントを3重に冗長化すると:

$$ R_{\text{parallel}}(t) = 1 – (1 – 0.9)^3 = 1 – 0.001 = 0.999 $$

信頼度が 0.9 から 0.999 へ、2桁向上しました。

関節の冗長性とタスク空間

ロボットアームの文脈で「冗長」とは、タスクを遂行するために必要な最小自由度よりも多くの関節を持つ ことを意味します。3次元空間での位置と姿勢を完全に指定するには6自由度が必要ですが、7自由度以上のロボットアームは 運動学的冗長 と呼ばれます。

$m$ 次元のタスク空間(手先の位置・姿勢)に対して $n$ 自由度のロボットアーム($n > m$)を考えます。順運動学は:

$$ \bm{x} = f(\bm{q}) $$

ここで $\bm{q} \in \mathbb{R}^n$ は関節角度ベクトル、$\bm{x} \in \mathbb{R}^m$ はタスク空間の手先位置・姿勢です。この微分関係を表すヤコビ行列 $\bm{J}$ は:

$$ \dot{\bm{x}} = \bm{J}(\bm{q}) \dot{\bm{q}}, \quad \bm{J} \in \mathbb{R}^{m \times n} $$

$n > m$ のとき、$\bm{J}$ は横長行列(行数 < 列数)となり、$\dot{\bm{x}}$ を実現する $\dot{\bm{q}}$ は一意に定まりません。この自由度の余裕 $(n – m)$ 次元が、故障時に活用できる 冗長自由度 です。

冗長性を活用した故障バイパス

7自由度ロボットアームの第 $k$ 関節が故障して固定されたとしましょう。故障関節の角速度を $\dot{q}_k = 0$ に拘束することは、ヤコビ行列の第 $k$ 列を除去することに相当します。

故障前のヤコビ行列を $\bm{J} \in \mathbb{R}^{6 \times 7}$ とすると、故障後のヤコビ行列は:

$$ \bm{J}_{\text{fault}} = [\bm{j}_1, \dots, \bm{j}_{k-1}, \bm{j}_{k+1}, \dots, \bm{j}_7] \in \mathbb{R}^{6 \times 6} $$

ここで $\bm{j}_i$ は $\bm{J}$ の第 $i$ 列です。もし $\bm{J}_{\text{fault}}$ がフルランク($\text{rank}(\bm{J}_{\text{fault}}) = 6$)であれば、残りの6関節で6次元のタスク空間をカバーでき、ロボットは本来の機能を維持できます。

逆に、$\bm{J}_{\text{fault}}$ がランク落ちする場合、ある方向への手先運動が不可能になります。どの関節が故障するとランク落ちが生じるかを事前に解析しておくことが、フォールトトレラントなロボット設計の重要なステップです。

この冗長性の議論は「故障しない設計」ではなく「故障後も機能を維持する設計」を可能にするものです。しかし、故障が起きたことをそもそもどうやって検知するのでしょうか? 次のセクションでは、故障検知・隔離・回復(FDIR)の体系的手法を解説します。

故障検知・隔離・回復(FDIR)

故障に備えた冗長設計を持っていても、故障が起きたことに気づけなければ対処のしようがありません。人間の体に例えると、痛みを感じる神経がなければ怪我に気づけないのと同じです。FDIR(Fault Detection, Isolation, and Recovery)は、宇宙システムにおける「痛覚」の役割を果たします。

FDIRの3段階

FDIRは名前の通り、3つの段階で構成されます。

1. 故障検知(Fault Detection): システムの振る舞いが正常範囲から逸脱していることを検出します。「何かがおかしい」ことは分かるが、まだどこが壊れたかは特定できていない段階です。

2. 故障隔離(Fault Isolation): 検知された異常の原因となっているコンポーネントを特定します。「関節3のエンコーダが異常」のように、故障箇所を絞り込みます。

3. 故障回復(Fault Recovery): 特定された故障コンポーネントをシステムから切り離し、冗長系に切り替えるなどして、システムの機能を回復します。

残差ベースの故障検知

故障検知の最も基本的なアプローチは 残差(residual) に基づく方法です。システムの数学モデルから予測される出力 $\hat{\bm{y}}$ と、実際のセンサ出力 $\bm{y}$ の差(残差)を監視します。

$$ \bm{r}(t) = \bm{y}(t) – \hat{\bm{y}}(t) $$

正常時には、残差はセンサノイズ程度の小さな値にとどまります。故障が発生すると、モデルの予測と実際の出力がずれるため、残差が大きくなります。

残差が閾値 $\tau$ を超えた場合に故障を宣言します:

$$ \|\bm{r}(t)\| > \tau \implies \text{故障検知} $$

閾値 $\tau$ の設定は 誤報率と検出漏れのトレードオフ です。$\tau$ を小さくすればほぼ全ての故障を検出できますが、ノイズによる誤報(False Alarm)が増えます。逆に $\tau$ を大きくすると誤報は減りますが、小さな故障を見逃す(Missed Detection)リスクが高まります。

状態オブザーバによる故障検知

より洗練された手法として、状態オブザーバ を用いた故障検知があります。ロボットの状態空間モデルを考えます:

$$ \dot{\bm{x}} = \bm{A}\bm{x} + \bm{B}\bm{u} + \bm{E}\bm{f} $$

$$ \bm{y} = \bm{C}\bm{x} $$

ここで $\bm{x}$ は状態ベクトル、$\bm{u}$ は制御入力、$\bm{f}$ は故障ベクトル、$\bm{E}$ は故障の影響方向を表す行列です。正常時には $\bm{f} = \bm{0}$ です。

ルーエンバーガーオブザーバを構成します:

$$ \dot{\hat{\bm{x}}} = \bm{A}\hat{\bm{x}} + \bm{B}\bm{u} + \bm{L}(\bm{y} – \bm{C}\hat{\bm{x}}) $$

ここで $\bm{L}$ はオブザーバゲインです。推定誤差 $\bm{e} = \bm{x} – \hat{\bm{x}}$ のダイナミクスは:

上の2つの式の差をとると:

$$ \dot{\bm{e}} = (\bm{A} – \bm{L}\bm{C})\bm{e} + \bm{E}\bm{f} $$

$\bm{A} – \bm{L}\bm{C}$ が安定(全固有値の実部が負)になるように $\bm{L}$ を設計すれば、正常時($\bm{f} = \bm{0}$)には $\bm{e} \to \bm{0}$ となり、残差 $\bm{r} = \bm{y} – \bm{C}\hat{\bm{x}} = \bm{C}\bm{e}$ もゼロに収束します。故障発生時($\bm{f} \neq \bm{0}$)には残差が大きくなり、故障を検知できます。

故障隔離の原理

故障検知だけでは「どこかが壊れた」しか分かりません。故障箇所を特定するために、構造化残差 を用います。

$n$ 個のコンポーネントに対して $n$ 個のオブザーバを設計し、各オブザーバが特定の故障に対して感度を持つ(または持たない)ように構造化します。具体的には、第 $i$ オブザーバの残差 $r_i$ が第 $j$ コンポーネントの故障に対して:

$$ r_i \neq 0 \iff j \in S_i $$

となるように設計します。ここで $S_i$ は第 $i$ オブザーバが感度を持つ故障の集合です。各オブザーバの残差パターン(どの残差が非ゼロか)を比較することで、故障箇所を一意に特定できます。

例えば、3つの関節に対して以下の残差テーブルを設計したとします:

故障箇所 $r_1$ $r_2$ $r_3$
関節1 $\neq 0$ $0$ $\neq 0$
関節2 $\neq 0$ $\neq 0$ $0$
関節3 $0$ $\neq 0$ $\neq 0$

残差パターンが $(r_1 \neq 0, r_2 \neq 0, r_3 = 0)$ であれば、関節2の故障と特定できます。

故障回復の戦略

故障箇所が特定された後の回復戦略は、大きく3種類に分けられます。

物理的冗長切替: 故障したコンポーネントの代わりに、待機していたバックアップを起動します。センサやコンピュータではこの方式が一般的です(コールドスタンバイ/ホットスタンバイ)。

機能的再構成: ロボットの運動学を再計算し、故障した関節をバイパスして残りの関節で任務を遂行します。次のセクションで詳しく解説します。

性能縮退運転(Graceful Degradation): 完全な機能回復が不可能な場合、性能を落として運用を続けます。例えば、手先速度の最大値を下げる、到達可能なワークスペースを縮小するなどです。

FDIRの3段階を経て、故障箇所を特定し回復戦略が決まったとします。ロボットアームの場合、最も重要な回復手法が「運動学的再構成」です。故障後にヤコビ行列をどう再計算し、どのように制御を切り替えるのかを見ていきましょう。

運動学的再構成 — 故障後のヤコビ行列再計算

運動学的再構成とは、故障した関節を「存在しないもの」として扱い、残りの関節だけでロボットの運動学とヤコビ行列を再定義する手法です。鍵盤楽器で指を1本怪我したとき、残りの指で演奏を続けるために運指を組み替えるのに似ています。

故障関節のロック

第 $k$ 関節が故障して角度 $q_k^*$ で固定(ロック)されたとします。この場合、ロボットの関節ベクトルから第 $k$ 要素を除去します:

$$ \bm{q}_{\text{red}} = (q_1, \dots, q_{k-1}, q_{k+1}, \dots, q_n)^T \in \mathbb{R}^{n-1} $$

順運動学は $q_k = q_k^*$ を代入した形になります:

$$ \bm{x} = f_{\text{red}}(\bm{q}_{\text{red}}) = f(q_1, \dots, q_{k-1}, q_k^*, q_{k+1}, \dots, q_n) $$

縮退ヤコビ行列の導出

縮退した順運動学を微分すると、縮退ヤコビ行列 $\bm{J}_{\text{red}}$ が得られます:

$$ \dot{\bm{x}} = \bm{J}_{\text{red}}(\bm{q}_{\text{red}}) \dot{\bm{q}}_{\text{red}} $$

具体的には、元のヤコビ行列 $\bm{J} \in \mathbb{R}^{m \times n}$ から第 $k$ 列を削除することで得られます:

$$ \bm{J}_{\text{red}} = [\bm{j}_1, \dots, \bm{j}_{k-1}, \bm{j}_{k+1}, \dots, \bm{j}_n] \in \mathbb{R}^{m \times (n-1)} $$

ただし、残りの列ベクトル $\bm{j}_i$ は $q_k = q_k^*$ のもとで再評価する必要があります。DH(Denavit-Hartenberg)パラメータを用いた一般的なヤコビ行列の計算では、各列は:

回転関節の場合:

$$ \bm{j}_i = \begin{pmatrix} \bm{z}_{i-1} \times (\bm{p}_n – \bm{p}_{i-1}) \\ \bm{z}_{i-1} \end{pmatrix} $$

ここで $\bm{z}_{i-1}$ は第 $i$ 関節の回転軸方向、$\bm{p}_{i-1}$ は第 $i$ 関節の位置、$\bm{p}_n$ は手先位置です。故障関節の角度が $q_k^*$ に固定されることで、$\bm{z}_{i-1}$, $\bm{p}_{i-1}$, $\bm{p}_n$ の値が変わるため、残りの列も全て再計算が必要です。

逆運動学の再構成

縮退ヤコビ行列を用いた逆運動学は、元の関節数と故障後の関節数の関係で場合分けされます。

Case 1: $n – 1 > m$ (まだ冗長性が残っている場合)

擬似逆行列を用いた最小ノルム解が利用できます:

$$ \dot{\bm{q}}_{\text{red}} = \bm{J}_{\text{red}}^{\dagger} \dot{\bm{x}} + (\bm{I} – \bm{J}_{\text{red}}^{\dagger}\bm{J}_{\text{red}})\bm{z} $$

右辺第2項は残りの冗長性を活用するための零空間射影で、$\bm{z}$ は任意のベクトルです。この余裕を使って、例えば関節可動範囲の中央に留まるなどの副次目標を達成できます。

Case 2: $n – 1 = m$ (冗長性がちょうどなくなった場合)

$\bm{J}_{\text{red}}$ が正方行列($m \times m$)になります。$\bm{J}_{\text{red}}$ が正則($\det(\bm{J}_{\text{red}}) \neq 0$)であれば:

$$ \dot{\bm{q}}_{\text{red}} = \bm{J}_{\text{red}}^{-1} \dot{\bm{x}} $$

と一意に求まります。ただし、特異姿勢($\det(\bm{J}_{\text{red}}) \approx 0$)の近傍では数値的に不安定になるため、ダンピング付き擬似逆行列(DLS: Damped Least Squares) を使います:

$$ \dot{\bm{q}}_{\text{red}} = \bm{J}_{\text{red}}^T(\bm{J}_{\text{red}}\bm{J}_{\text{red}}^T + \lambda^2 \bm{I})^{-1} \dot{\bm{x}} $$

ここで $\lambda$ はダンピング係数です。特異姿勢近傍での発散を防ぐ代わりに、追従精度がやや低下します。

Case 3: $n – 1 < m$ (自由度が不足する場合)

タスク空間の全方向への運動が不可能になります。この場合、タスクの優先度を設定し、高優先度のタスク(例えば位置制御)を優先して低優先度のタスク(例えば姿勢制御)を犠牲にするなどの戦略が必要です。

可操作度による性能評価

故障前後の性能を定量的に比較するために、可操作度(manipulability) を用います。吉川の可操作度は:

$$ w = \sqrt{\det(\bm{J}\bm{J}^T)} $$

$w$ は手先がどれだけ自由に動けるかの指標で、$w = 0$ は特異姿勢(ある方向に動けなくなる姿勢)を意味します。故障後の可操作度は:

$$ w_{\text{fault}} = \sqrt{\det(\bm{J}_{\text{red}}\bm{J}_{\text{red}}^T)} $$

故障によって可操作度がどの程度低下するかは、故障した関節と現在の姿勢に依存します。可操作度を姿勢空間全体にわたって計算し、故障後も $w_{\text{fault}} > w_{\min}$(最低限の可操作度)を維持できる領域を フォールトトレラントワークスペース と定義できます。

運動学的再構成によって「故障後にどう動くか」の計算が可能になりました。しかし、再構成された運動学のもとで制御器をどう設計すべきでしょうか? 次のセクションでは、故障後の制御系設計であるフォールトトレラント制御について解説します。

フォールトトレラント制御

フォールトトレラント制御(FTC: Fault-Tolerant Control)は、故障が検知された後、制御系を再構成して 故障下でも許容可能な性能を維持する 制御理論の一分野です。完全な性能維持が理想ですが、実際には「性能は低下するが任務は継続できる」というグレースフルデグラデーションが現実的な目標になります。

パッシブFTC vs アクティブFTC

フォールトトレラント制御は大きく2つのアプローチに分類されます。

パッシブFTC(Passive FTC): あらかじめ想定される故障シナリオ全てに対してロバストな1つの制御器を設計しておく方法です。故障検知機構は不要ですが、全ての故障シナリオに対応できる保守的な設計となるため、正常時の性能が犠牲になります。

数学的には、正常モードと各故障モードを含む ポリトープ不確かさ として表現し、全頂点で安定性と性能を保証する制御器を設計します:

$$ \dot{\bm{x}} = \bm{A}_i \bm{x} + \bm{B}_i \bm{u}, \quad i = 0, 1, \dots, N $$

ここで $i = 0$ は正常モード、$i = 1, \dots, N$ は各故障モードです。全てのモードで安定化するフィードバックゲイン $\bm{K}$ を求めるために、共通のリアプノフ関数 $V(\bm{x}) = \bm{x}^T \bm{P} \bm{x}$ が存在する条件:

$$ (\bm{A}_i + \bm{B}_i \bm{K})^T \bm{P} + \bm{P}(\bm{A}_i + \bm{B}_i \bm{K}) < \bm{0}, \quad \forall i $$

これは $\bm{P} > \bm{0}$ と合わせて 線形行列不等式(LMI) として解くことができます。

アクティブFTC(Active FTC): 故障をリアルタイムに検知し、故障の種類と程度に応じて制御器を再構成する方法です。前セクションで解説したFDIRと連携して動作します。正常時には最適な性能を発揮しつつ、故障時には制御器パラメータを適応的に変更します。

アクティブFTCの再構成

アクティブFTCの一般的なフレームワークを考えます。正常時の制御入力を:

$$ \bm{u} = \bm{K}_0 \bm{x} + \bm{u}_{\text{ff}} $$

とします。ここで $\bm{K}_0$ はフィードバックゲイン、$\bm{u}_{\text{ff}}$ はフィードフォワード項です。

第 $k$ 関節が故障した場合、制御入力ベクトルから第 $k$ 要素を除去し、新たなフィードバックゲイン $\bm{K}_k$ を用いて:

$$ \bm{u}_{\text{red}} = \bm{K}_k \bm{x} + \bm{u}_{\text{ff,red}} $$

と再構成します。$\bm{K}_k$ は、縮退したシステムに対して改めて極配置やLQR(線形二次レギュレータ)で設計します。

制御アロケーション

関節の冗長性がある場合、同じタスク空間の力・トルクを実現する関節トルクの組み合わせは無数にあります。制御アロケーション(Control Allocation) は、この自由度を活用して、故障関節を避けつつ所望のタスク空間力を発生させる手法です。

タスク空間での所望の力 $\bm{F}_{\text{des}} \in \mathbb{R}^m$ を関節トルク $\bm{\tau} \in \mathbb{R}^n$ で実現するには:

$$ \bm{F}_{\text{des}} = \bm{J}^T \bm{\tau} $$

ここで $\bm{J}^T$ は転置ヤコビ行列です。$n > m$ のとき、最小ノルム解は:

$$ \bm{\tau} = \bm{J}^{T\dagger} \bm{F}_{\text{des}} = \bm{J}(\bm{J}^T\bm{J})^{-1} \bm{F}_{\text{des}} $$

第 $k$ 関節が故障した場合、制約 $\tau_k = 0$(故障関節にトルクを出せない)を追加し、制約付き最適化問題として解きます:

$$ \min_{\bm{\tau}} \|\bm{\tau}\|^2 \quad \text{s.t.} \quad \bm{J}^T \bm{\tau} = \bm{F}_{\text{des}}, \quad \tau_k = 0 $$

これはラグランジュの未定乗数法で解析的に解くことができます。

制御レベルでの対策に加えて、ソフトウェア自体の信頼性も宇宙ロボットにとって重要な課題です。次のセクションでは、ソフトウェアレベルでのフォールトトレランス手法を見ていきます。

ソフトウェアフォールトトレランス

ここまではハードウェア(関節やセンサ)の故障を扱ってきましたが、宇宙ロボットのソフトウェアも故障(バグ、放射線によるメモリ破損など)の原因となります。実際、宇宙ミッションの故障の約30〜40%はソフトウェアに起因するとされています。ソフトウェアフォールトトレランスは、ソフトウェアの故障に対してシステムの正常動作を維持する技術です。

N版プログラミング(N-Version Programming)

同じ仕様に基づいて 独立した開発チーム が $N$ 個の異なるバージョンのソフトウェアを実装し、全てのバージョンを同時に実行して 多数決 で最終出力を決定する手法です。

各バージョンは異なるアルゴリズム、プログラミング言語、開発環境で実装されるため、同じバグが複数のバージョンに現れる確率は低いという前提に基づきます。

$N = 3$ のとき(三重冗長)、各バージョンの出力を $y_1, y_2, y_3$ とすると、多数決ロジックは:

$$ y_{\text{out}} = \text{majority}(y_1, y_2, y_3) = \begin{cases} y_i & \text{if } y_i = y_j \text{ for some } i \neq j \\ \text{error} & \text{if all differ} \end{cases} $$

連続値の場合は、中央値(メディアン)を取る方法が一般的です:

$$ y_{\text{out}} = \text{median}(y_1, y_2, y_3) $$

リカバリーブロック

N版プログラミングがハードウェア冗長の「並列」に対応するならば、リカバリーブロックは「直列」に対応する考え方です。

  1. 主系(Primary) のソフトウェアを実行
  2. 出力を 受入テスト(Acceptance Test) で検証
  3. テストに合格すれば出力を採用
  4. 不合格なら 代替系(Alternate) を実行
  5. 代替系の出力を受入テストで検証
  6. 必要なら第3、第4の代替系へ

擬似コードで表すと:

ensure <acceptance_test>
    by <primary_algorithm>
    else by <alternate_algorithm_1>
    else by <alternate_algorithm_2>
    else error

宇宙ロボットの軌道計画では、主系が最適経路を計算し、受入テストが障害物との最小距離や関節可動範囲をチェックします。主系が放射線でメモリ破損して異常な経路を出力した場合、受入テストが不合格とし、別アルゴリズムの代替系に切り替えます。

ウォッチドッグタイマーとハートビート

最もシンプルだが極めて有効なソフトウェアフォールトトレランス機構が ウォッチドッグタイマー です。制御ソフトウェアが一定時間 $T_{\text{wd}}$ 以内にタイマーをリセットしなければ、ハードウェアレベルで強制的にシステムをリセットします。

宇宙ロボットでは、各サブシステム(関節制御器、姿勢制御器、通信モジュール)が定期的に ハートビート信号 を中央管理システムに送信し、応答が途絶えたサブシステムを自動的に再起動または切り離す設計が一般的です。

ECC(Error Correcting Code)メモリ

放射線によるSEU対策として、ハミングコード などの誤り訂正符号を用いたECCメモリが宇宙用コンピュータには標準的に搭載されます。$k$ ビットのデータに $r$ ビットの冗長ビットを追加し、1ビットの誤りを自動訂正、2ビットの誤りを検出できます。

$$ r \geq \log_2(k + r + 1) $$

例えば64ビットデータに8ビットの冗長ビット(合計72ビット)を追加するSECDED(Single Error Correction, Double Error Detection)方式が広く使われています。

ここまでで、ハードウェアからソフトウェアまで、宇宙ロボットのフォールトトレランスを体系的に解説してきました。次のセクションでは、これらの理論をPythonシミュレーションで具体的に確認します。特に、関節故障時に可操作度がどのように低下するかを視覚的に理解しましょう。

Pythonシミュレーション — 関節故障時の可操作度低下

ここでは、平面7自由度ロボットアーム(7Rマニピュレータ)を題材に、各関節が故障したときの可操作度の変化をシミュレーションします。平面ロボットではタスク空間は2次元($x, y$ 位置)なので、7自由度のうち5自由度が冗長です。1関節が故障しても十分な冗長性が残るはずですが、故障する関節の位置や姿勢によって性能低下の度合いが大きく異なることを確認しましょう。

まず、平面ロボットアームの順運動学とヤコビ行列を計算する関数を実装します。

import numpy as np
import matplotlib.pyplot as plt

def forward_kinematics(q, link_lengths):
    """平面ロボットアームの順運動学

    Parameters
    ----------
    q : array-like
        関節角度 [rad]
    link_lengths : array-like
        リンク長

    Returns
    -------
    positions : ndarray, shape (n+1, 2)
        各関節と手先の位置
    """
    n = len(q)
    positions = np.zeros((n + 1, 2))
    angle_sum = 0.0
    for i in range(n):
        angle_sum += q[i]
        positions[i + 1, 0] = positions[i, 0] + link_lengths[i] * np.cos(angle_sum)
        positions[i + 1, 1] = positions[i, 1] + link_lengths[i] * np.sin(angle_sum)
    return positions


def compute_jacobian(q, link_lengths):
    """平面ロボットアームのヤコビ行列(位置のみ、2×n)

    Parameters
    ----------
    q : array-like
        関節角度 [rad]
    link_lengths : array-like
        リンク長

    Returns
    -------
    J : ndarray, shape (2, n)
        ヤコビ行列
    """
    n = len(q)
    J = np.zeros((2, n))
    for i in range(n):
        angle_sum = np.sum(q[:i + 1])
        for j in range(i, n):
            # 関節iは関節i以降のリンク全てに影響
            pass
    # 正しい計算: 各列は手先位置の各関節角度に対する偏微分
    positions = forward_kinematics(q, link_lengths)
    end_effector = positions[-1]
    for i in range(n):
        angle_sum = np.sum(q[:i + 1])
        # 関節iから手先までの相対位置
        rel_x = end_effector[0] - positions[i, 0]
        rel_y = end_effector[1] - positions[i, 1]
        J[0, i] = -rel_y  # dx/dq_i
        J[1, i] = rel_x   # dy/dq_i
    return J


def manipulability(J):
    """可操作度 w = sqrt(det(J @ J^T)) を計算"""
    JJT = J @ J.T
    det_val = np.linalg.det(JJT)
    return np.sqrt(max(det_val, 0.0))

次に、正常時と各関節故障時の可操作度を比較するシミュレーションを実装します。故障関節をロック(速度ゼロに固定)することは、ヤコビ行列から対応する列を削除することに相当します。

# ロボットアームの設定
n_joints = 7
link_lengths = np.array([1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4])

# 様々な姿勢でサンプリングして可操作度を比較
np.random.seed(42)
n_samples = 2000

# 各関節故障時の可操作度低下率を記録
manipulability_ratios = {f"関節{i+1}故障": [] for i in range(n_joints)}

for _ in range(n_samples):
    # ランダムな姿勢を生成
    q = np.random.uniform(-np.pi / 2, np.pi / 2, n_joints)

    # 正常時のヤコビ行列と可操作度
    J_normal = compute_jacobian(q, link_lengths)
    w_normal = manipulability(J_normal)

    if w_normal < 1e-6:
        continue  # 元から特異姿勢に近い場合はスキップ

    # 各関節を故障させた場合
    for k in range(n_joints):
        # 第k列を削除(故障関節をロック)
        J_fault = np.delete(J_normal, k, axis=1)
        w_fault = manipulability(J_fault)
        ratio = w_fault / w_normal
        manipulability_ratios[f"関節{k+1}故障"].append(ratio)
# 可操作度低下率の箱ひげ図
fig, ax = plt.subplots(figsize=(10, 6))
labels = [f"Joint {i+1}\nfault" for i in range(n_joints)]
data = [manipulability_ratios[f"関節{i+1}故障"] for i in range(n_joints)]

bp = ax.boxplot(data, labels=labels, patch_artist=True,
                boxprops=dict(facecolor='steelblue', alpha=0.7),
                medianprops=dict(color='orange', linewidth=2))

ax.set_ylabel("Manipulability Ratio (fault / normal)", fontsize=12)
ax.set_xlabel("Faulty Joint", fontsize=12)
ax.set_title("Manipulability Degradation by Joint Failure\n(7-DOF Planar Robot, 2000 random configurations)", fontsize=13)
ax.axhline(y=1.0, color='red', linestyle='--', alpha=0.5, label='No degradation')
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("manipulability_degradation_boxplot.png", dpi=150)
plt.show()

上の箱ひげ図から、いくつかの重要な特徴が読み取れます。

  1. 根元に近い関節(関節1、2)の故障は影響が大きい — 根元の関節は全てのリンクの運動に寄与するため、ヤコビ行列への寄与が大きく、故障時の可操作度低下が顕著です。中央値で見ても、正常時の50〜60%程度まで低下する場合があります。
  2. 先端に近い関節(関節6、7)の故障は影響が小さい — 先端のリンクは短く、手先位置への寄与が限定的です。故障しても可操作度は80〜90%以上を維持する場合が多いです。
  3. 姿勢による変動が大きい — 各関節の箱ひげ図のばらつき(四分位範囲)が大きく、同じ関節の故障でも姿勢によって影響が大きく異なることがわかります。これは、フォールトトレラント設計では特定の姿勢での最悪ケースを考慮する必要があることを示しています。

続いて、特定の姿勢での可操作度楕円体を故障前後で比較します。

def plot_manipulability_ellipse(J, center, ax, color='blue', label='', alpha=0.3):
    """可操作度楕円体を描画"""
    JJT = J @ J.T
    eigenvalues, eigenvectors = np.linalg.eigh(JJT)
    eigenvalues = np.maximum(eigenvalues, 0)

    # 楕円のパラメータ
    angle = np.arctan2(eigenvectors[1, 1], eigenvectors[0, 1])
    width = 2 * np.sqrt(eigenvalues[1])
    height = 2 * np.sqrt(eigenvalues[0])

    from matplotlib.patches import Ellipse
    ellipse = Ellipse(xy=center, width=width, height=height,
                      angle=np.degrees(angle), alpha=alpha,
                      facecolor=color, edgecolor=color, linewidth=2,
                      label=label)
    ax.add_patch(ellipse)


# 特定の姿勢を設定
q_demo = np.array([0.3, 0.5, -0.2, 0.4, -0.3, 0.2, -0.1])
positions = forward_kinematics(q_demo, link_lengths)
end_effector = positions[-1]

J_normal = compute_jacobian(q_demo, link_lengths)
w_normal = manipulability(J_normal)

fig, axes = plt.subplots(2, 4, figsize=(16, 8))

for idx in range(n_joints):
    row = idx // 4
    col = idx % 4
    ax = axes[row, col]

    # ロボットアームを描画
    ax.plot(positions[:, 0], positions[:, 1], 'k-o', linewidth=2,
            markersize=4, zorder=3)
    # 故障関節を赤で表示
    ax.plot(positions[idx, 0], positions[idx, 1], 'ro', markersize=10,
            zorder=4, label=f'Faulty joint {idx+1}')

    # 正常時の楕円
    plot_manipulability_ellipse(J_normal, end_effector, ax,
                                color='blue', label=f'Normal (w={w_normal:.2f})',
                                alpha=0.2)

    # 故障時の楕円
    J_fault = np.delete(J_normal, idx, axis=1)
    w_fault = manipulability(J_fault)
    plot_manipulability_ellipse(J_fault, end_effector, ax,
                                color='red', label=f'Fault (w={w_fault:.2f})',
                                alpha=0.3)

    ax.set_xlim(-2, 5)
    ax.set_ylim(-2, 4)
    ax.set_aspect('equal')
    ax.legend(fontsize=7, loc='upper left')
    ax.set_title(f"Joint {idx+1} failure", fontsize=10)
    ax.grid(True, alpha=0.3)

# 8番目のサブプロットを使って凡例の説明
axes[1, 3].axis('off')
axes[1, 3].text(0.5, 0.5, "Blue: Normal\nRed: After fault\n\n"
                "Ellipse area is\nproportional to\nmanipulability",
                ha='center', va='center', fontsize=12,
                transform=axes[1, 3].transAxes,
                bbox=dict(boxstyle='round', facecolor='lightyellow'))

fig.suptitle("Manipulability Ellipses: Normal vs Joint Failure\n"
             "(7-DOF Planar Robot)", fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig("manipulability_ellipses.png", dpi=150)
plt.show()

可操作度楕円体の比較から、故障の影響が視覚的に明確にわかります。

  1. 楕円の面積が可操作度に対応 しています。青い楕円(正常時)に比べて赤い楕円(故障時)が小さいほど、手先の運動能力が大きく低下していることを意味します。
  2. 楕円の形状の歪みにも注目 してください。面積が大きくても楕円が細長くなっている場合、特定方向への運動能力が極端に低下しており、実用上の問題が生じます。
  3. 根元の関節(関節1)の故障 では楕円が大幅に縮小するのに対し、先端の関節(関節7)の故障 では楕円の変化が小さいことが、前の統計的解析と一致しています。

最後に、故障前後でのロボットアームの軌道追従を比較するシミュレーションを実装します。

def inverse_kinematics_step(J, x_dot, damping=0.01):
    """ダンピング付き擬似逆行列による逆運動学の1ステップ"""
    JJT = J @ J.T
    J_dls = J.T @ np.linalg.inv(JJT + damping**2 * np.eye(JJT.shape[0]))
    return J_dls @ x_dot


# 円軌道追従シミュレーション
dt = 0.01
t_total = 4.0
t = np.arange(0, t_total, dt)

# 手先で描く円軌道の中心と半径
q_init = np.array([0.4, 0.3, -0.1, 0.3, -0.2, 0.15, -0.05])
pos_init = forward_kinematics(q_init, link_lengths)[-1]
circle_center = pos_init
circle_radius = 0.5
omega = 2 * np.pi / t_total  # 1周する角速度

# 目標軌道
x_des = circle_center[0] + circle_radius * np.cos(omega * t)
y_des = circle_center[1] + circle_radius * np.sin(omega * t)

# 正常時の軌道追従
q_traj_normal = np.zeros((len(t), n_joints))
q_traj_normal[0] = q_init.copy()
ee_traj_normal = np.zeros((len(t), 2))
ee_traj_normal[0] = pos_init

for step in range(1, len(t)):
    q_curr = q_traj_normal[step - 1]
    J_curr = compute_jacobian(q_curr, link_lengths)

    # 目標速度
    x_dot_des = np.array([
        -circle_radius * omega * np.sin(omega * t[step]),
        circle_radius * omega * np.cos(omega * t[step])
    ])

    # フィードバック補正
    pos_curr = forward_kinematics(q_curr, link_lengths)[-1]
    pos_des = np.array([x_des[step], y_des[step]])
    x_dot = x_dot_des + 5.0 * (pos_des - pos_curr)

    q_dot = inverse_kinematics_step(J_curr, x_dot)
    q_traj_normal[step] = q_curr + q_dot * dt
    ee_traj_normal[step] = forward_kinematics(q_traj_normal[step], link_lengths)[-1]

# 関節2故障時の軌道追従
fault_joint = 1  # 0-indexed: 関節2
q_traj_fault = np.zeros((len(t), n_joints))
q_traj_fault[0] = q_init.copy()
ee_traj_fault = np.zeros((len(t), 2))
ee_traj_fault[0] = pos_init

for step in range(1, len(t)):
    q_curr = q_traj_fault[step - 1]
    J_curr = compute_jacobian(q_curr, link_lengths)

    # 故障関節の列を削除
    J_fault = np.delete(J_curr, fault_joint, axis=1)

    # 目標速度
    x_dot_des = np.array([
        -circle_radius * omega * np.sin(omega * t[step]),
        circle_radius * omega * np.cos(omega * t[step])
    ])

    # フィードバック補正
    pos_curr = forward_kinematics(q_curr, link_lengths)[-1]
    pos_des = np.array([x_des[step], y_des[step]])
    x_dot = x_dot_des + 5.0 * (pos_des - pos_curr)

    q_dot_red = inverse_kinematics_step(J_fault, x_dot)

    # 故障関節以外を更新
    q_new = q_curr.copy()
    active_indices = [i for i in range(n_joints) if i != fault_joint]
    for i, idx in enumerate(active_indices):
        q_new[idx] = q_curr[idx] + q_dot_red[i] * dt
    q_traj_fault[step] = q_new
    ee_traj_fault[step] = forward_kinematics(q_traj_fault[step], link_lengths)[-1]
# 軌道追従の比較プロット
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 左: XY平面での軌道
ax1 = axes[0]
ax1.plot(x_des, y_des, 'g--', linewidth=2, label='Desired trajectory')
ax1.plot(ee_traj_normal[:, 0], ee_traj_normal[:, 1], 'b-',
         linewidth=1.5, label='Normal (7 joints)')
ax1.plot(ee_traj_fault[:, 0], ee_traj_fault[:, 1], 'r-',
         linewidth=1.5, label='Joint 2 locked (6 joints)')
ax1.set_xlabel("x [m]", fontsize=12)
ax1.set_ylabel("y [m]", fontsize=12)
ax1.set_title("End-Effector Trajectory", fontsize=13)
ax1.legend(fontsize=11)
ax1.set_aspect('equal')
ax1.grid(True, alpha=0.3)

# 右: 追従誤差の時間変化
error_normal = np.sqrt((ee_traj_normal[:, 0] - x_des)**2 +
                        (ee_traj_normal[:, 1] - y_des)**2)
error_fault = np.sqrt((ee_traj_fault[:, 0] - x_des)**2 +
                       (ee_traj_fault[:, 1] - y_des)**2)

ax2 = axes[1]
ax2.plot(t, error_normal * 1000, 'b-', linewidth=1.5, label='Normal (7 joints)')
ax2.plot(t, error_fault * 1000, 'r-', linewidth=1.5, label='Joint 2 locked (6 joints)')
ax2.set_xlabel("Time [s]", fontsize=12)
ax2.set_ylabel("Tracking Error [mm]", fontsize=12)
ax2.set_title("Tracking Error Over Time", fontsize=13)
ax2.legend(fontsize=11)
ax2.grid(True, alpha=0.3)

plt.suptitle("Trajectory Tracking: Normal vs Fault Condition", fontsize=14,
             fontweight='bold')
plt.tight_layout()
plt.savefig("trajectory_tracking_comparison.png", dpi=150)
plt.show()

print(f"正常時の平均追従誤差: {np.mean(error_normal)*1000:.2f} mm")
print(f"故障時の平均追従誤差: {np.mean(error_fault)*1000:.2f} mm")
print(f"追従誤差の増加率: {np.mean(error_fault)/np.mean(error_normal):.1f} 倍")

軌道追従シミュレーションの結果から、以下のことがわかります。

  1. 正常時(7関節) では、ダンピング付き擬似逆行列による制御で円軌道をほぼ完全に追従できます。追従誤差はミリメートルオーダーの非常に小さな値に収まります。
  2. 関節2故障時(6関節) でも、冗長性のおかげで円軌道の追従は可能です。しかし、追従誤差は正常時の数倍に増大します。これは、故障関節をバイパスするために残りの関節がより大きく動く必要があり、ヤコビ行列の条件数が悪化するためです。
  3. 誤差の時間変動 にも注目してください。故障時には特定の姿勢で誤差が急増する区間が見られます。これは、その姿勢で縮退ヤコビ行列が特異姿勢に近づいていることを示しており、運用時には姿勢計画で回避する必要があります。

この結果は、7自由度の冗長設計が関節故障時のグレースフルデグラデーションを可能にするという理論を数値的に裏付けています。ただし、性能低下は避けられないため、故障後は速度制限やワークスペースの縮小といった運用制約を適用することが実際のミッションでは重要です。

まとめ

本記事では、宇宙ロボットの信頼性とフォールトトレランス設計について、理論から実装まで体系的に解説しました。

  • 宇宙ロボットに信頼性が不可欠な理由: 修理の困難さ、ミッション全体への波及効果。直列システムではコンポーネント数に比例してシステム故障率が増大する
  • 宇宙環境の故障要因: 放射線(SEU、TID)、熱サイクル疲労、デブリ衝突、真空中の潤滑不良
  • 冗長設計: 並列冗長による信頼度向上、運動学的冗長性による故障バイパス
  • FDIR: 残差ベースの故障検知、構造化残差による故障隔離、回復戦略
  • 運動学的再構成: 故障関節のロック、縮退ヤコビ行列の導出、ダンピング付き擬似逆行列
  • フォールトトレラント制御: パッシブFTC(LMIベース)とアクティブFTC(制御アロケーション)
  • ソフトウェアフォールトトレランス: N版プログラミング、リカバリーブロック、ウォッチドッグタイマー
  • シミュレーション: 根元の関節故障ほど可操作度低下が大きく、先端の関節故障は影響が限定的

フォールトトレランス設計は、宇宙ロボットに限らず、自律システム全般の信頼性向上に不可欠な技術です。今後、軌道上サービス(on-orbit servicing)やデブリ除去ミッションでは、複数のロボットが協調して作業を行う場面が増えていきます。

次のステップとして、以下の記事も参考にしてください。