宇宙空間でロボットアームを動かすと、何が起こるでしょうか。地上のロボットは床にボルトで固定されているため、アームを振り回しても本体は動きません。しかし宇宙空間には「床」がありません。アームを動かせば、その反作用でベース衛星そのものが回転してしまいます。
これは単なる理論的な問題ではありません。ベース衛星が光学望遠鏡を搭載しているとしましょう。アーム操作のたびに衛星が数度でも回転すれば、望遠鏡の視線が目標天体からずれてしまい、長時間露光の観測は台無しになります。あるいは高利得アンテナを地上局に向けている最中にアームが動けば、通信リンクが途絶する危険があります。
このような問題を解決するのがリアクションレスマニューバ(reactionless maneuver, 反力なし運動)です。アームの関節を巧みに協調制御することで、ベース衛星にトルク反力を一切与えずに手先を目標位置に移動させる技術です。
リアクションレスマニューバを理解すると、以下のような応用が開けます。
- 宇宙望遠鏡の保守: ベース衛星の姿勢を乱さずにアームで機器交換やメンテナンスを行う(JWSTのような大型望遠鏡を想定)
- 国際宇宙ステーション(ISS)の運用: JEMRMSやCanadarm2での実験モジュール移設時に、ステーション全体の姿勢外乱を最小化する
- 軌道上サービス: 故障衛星への接近・捕獲でクライアント衛星を回転させない
- デブリ捕獲: デブリをつかんだ後の安定化操作で系全体の角運動量管理が不可欠
本記事の内容
- なぜ反力なし運動が必要か — 宇宙マニピュレータ特有の力学
- リアクションレスマニューバの条件 — ベース角運動量変化ゼロの数学的定式化
- 一般化ヤコビ行列(GJM)のヌル空間射影による運動計画
- 冗長自由度(7DOF以上)がなぜ必要か
- 並進反力と回転反力の分離
- リアクション最小化マニューバ — 完全にゼロにできない場合の対処
- Pythonによるシミュレーションと比較
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
- 冗長自由度マニピュレータと擬似逆行列 — ヌル空間射影の基本概念
- 一般化ヤコビ行列 — 宇宙マニピュレータの運動学
宇宙マニピュレータの反力問題 — 地上ロボットとの本質的な違い
地上ロボットのケース
地上の産業用ロボットを思い浮かべてください。アームの根元は工場の床に頑丈にボルト止めされています。アームを高速で振り回すと、反力(反トルク)は根元を通じて床に吸収されます。床は(事実上)無限の質量と慣性モーメントを持つため、微動だにしません。
このため、地上ロボットでは「アーム操作がベースに与える影響」を考える必要がほとんどありません。手先のヤコビ行列 $\bm{J}$ を使って逆運動学を解けば十分です。
宇宙マニピュレータのケース
ところが宇宙空間では状況が一変します。ベース衛星は自由浮遊(free-floating)しているため、外力・外トルクの合計がゼロなら運動量保存則と角運動量保存則が成立します。
アームの関節を回すと、関節モータが生成するトルクの反作用が必ずベースに伝わります。宇宙空間には「床」がないため、このトルク反力を吸収するものがありません。結果として、アームを動かせばベースが回転し、ベースを止めようとすればリアクションホイールやスラスタで姿勢制御する必要があります。
数学的には、系全体に外トルクが作用しない場合、系の全角運動量 $\bm{L}_{\text{total}}$ は保存されます。
$$ \bm{L}_{\text{total}} = \bm{L}_{\text{base}} + \bm{L}_{\text{arm}} = \text{const.} $$
ここで $\bm{L}_{\text{base}}$ はベース衛星の角運動量、$\bm{L}_{\text{arm}}$ はアーム全体の角運動量です。アームを動かして $\bm{L}_{\text{arm}}$ が変化すれば、保存則により $\bm{L}_{\text{base}}$ も必ず変化します。つまり、ベースの姿勢が変わってしまうのです。
実際の運用事例: JEMRMS
国際宇宙ステーション日本実験棟「きぼう」には、JEMRMS(JEM Remote Manipulator System)という6自由度のロボットアームが搭載されています。ISSの場合、ステーション全体の慣性モーメントがアームに比べてはるかに大きいため、姿勢外乱は小さく抑えられますが、それでも精密な実験中にはアーム操作のタイミングに配慮が必要です。
将来の小型衛星にロボットアームを搭載する場合、ベースとアームの慣性比が桁違いに小さくなるため、反力の問題はさらに深刻になります。
宇宙マニピュレータの反力問題の本質を理解したところで、次にこの問題を数学的に定式化し、リアクションレスマニューバの条件を導出しましょう。
ベース角運動量の定式化 — 宇宙マニピュレータの運動学
系の構成
ベース衛星(剛体)と $n$ 自由度のシリアルリンクマニピュレータから構成される宇宙ロボットシステムを考えます。以下の変数を定義します。
- $\bm{\theta} = (\theta_1, \theta_2, \ldots, \theta_n)^T$: 関節角度ベクトル
- $\bm{x}_0$: ベースの位置(3次元)
- $\bm{\phi}_0$: ベースの姿勢(3次元、オイラー角等)
- $\bm{I}_0$: ベースの慣性テンソル
- $\bm{\omega}_0$: ベースの角速度
系全体に外力・外トルクが作用しない自由浮遊条件を仮定します。
角運動量保存則の展開
系全体の角運動量はベースとアームの寄与の和です。ベースの角運動量は
$$ \bm{L}_{\text{base}} = \bm{I}_0 \bm{\omega}_0 $$
アームの角運動量は各リンクの寄与の総和です。第 $i$ リンクの質量を $m_i$、重心位置を $\bm{r}_i$、慣性テンソルを $\bm{I}_i$、角速度を $\bm{\omega}_i$ とすると
$$ \bm{L}_{\text{arm}} = \sum_{i=1}^{n} \left[ \bm{I}_i \bm{\omega}_i + m_i (\bm{r}_i – \bm{r}_G) \times \dot{\bm{r}}_i \right] $$
ここで $\bm{r}_G$ は系全体の重心位置です。自由浮遊系では系の重心は等速直線運動するため、重心を慣性系の原点に取れば、全角運動量が保存されます。
$$ \bm{L}_{\text{total}} = \bm{L}_{\text{base}} + \bm{L}_{\text{arm}} = \bm{L}_0 = \text{const.} $$
$\bm{L}_0$ は初期角運動量です。
角運動量と関節速度の関係
アームの各リンクの角速度 $\bm{\omega}_i$ と並進速度 $\dot{\bm{r}}_i$ は、ベースの運動と関節速度の線形結合で表現できます。具体的には
$$ \bm{\omega}_i = \bm{\omega}_0 + \sum_{j=1}^{i} \bm{e}_j \dot{\theta}_j $$
ここで $\bm{e}_j$ は第 $j$ 関節の回転軸の単位ベクトルです。この関係を角運動量の式に代入して整理すると、系全体の角運動量は次の形に書けます。
$$ \bm{L}_{\text{total}} = \bm{I}_s \bm{\omega}_0 + \bm{H}_\theta \dot{\bm{\theta}} $$
ここで各項の意味は次の通りです。
- $\bm{I}_s$: 系全体の(ベース原点まわりの)等価慣性テンソル。ベース自身の慣性 $\bm{I}_0$ とアーム各リンクの並進・回転慣性の寄与をまとめたもの
- $\bm{H}_\theta$: 関節速度 $\dot{\bm{\theta}}$ がベース原点まわりの角運動量に寄与する度合いを表す $3 \times n$ 行列(慣性結合行列と呼ばれます)
$\bm{I}_s$ の具体的な形は、各リンクの慣性テンソルと質量配分から以下のように構成されます。
$$ \bm{I}_s = \bm{I}_0 + \sum_{i=1}^{n} \left[ \bm{I}_i + m_i \widetilde{(\bm{r}_i – \bm{r}_0)}^T \widetilde{(\bm{r}_i – \bm{r}_0)} \right] $$
ここで $\widetilde{\bm{a}}$ はベクトル $\bm{a}$ の歪対称行列(外積演算に対応する行列)を表します。
同様に、$\bm{H}_\theta$ は次のように構成されます。
$$ \bm{H}_\theta = \sum_{i=1}^{n} \left[ \bm{I}_i \bm{J}_{\omega i} + m_i \widetilde{(\bm{r}_i – \bm{r}_0)} \bm{J}_{v i} \right] $$
ここで $\bm{J}_{\omega i}$ と $\bm{J}_{v i}$ は、関節速度に対する第 $i$ リンクの角速度と並進速度のヤコビ行列です。
ベース角速度の解消
角運動量保存則から、ベースの角速度を関節速度で表すことができます。
$$ \bm{I}_s \bm{\omega}_0 + \bm{H}_\theta \dot{\bm{\theta}} = \bm{L}_0 $$
$\bm{I}_s$ は正定値対称行列なので逆行列が存在します。$\bm{\omega}_0$ について解くと
$$ \bm{\omega}_0 = \bm{I}_s^{-1} (\bm{L}_0 – \bm{H}_\theta \dot{\bm{\theta}}) $$
特に、初期角運動量がゼロ($\bm{L}_0 = \bm{0}$)の場合は
$$ \bm{\omega}_0 = -\bm{I}_s^{-1} \bm{H}_\theta \dot{\bm{\theta}} $$
この式が宇宙マニピュレータの力学の核心です。アームの関節が動くと($\dot{\bm{\theta}} \neq \bm{0}$)、ベースの角速度は一般にゼロになりません。$-\bm{I}_s^{-1}\bm{H}_\theta$ という結合行列を通じて、関節運動がベースの回転に「漏れ出す」のです。
この定式化を踏まえて、次に一般化ヤコビ行列(GJM)を導入し、リアクションレスマニューバの条件を明確にしましょう。
一般化ヤコビ行列とリアクションレス条件
一般化ヤコビ行列(GJM)の復習
自由浮遊宇宙マニピュレータでは、手先の速度 $\dot{\bm{x}}_e$(位置と姿勢の6次元速度)は、関節速度 $\dot{\bm{\theta}}$ だけの関数として表現できます。これが一般化ヤコビ行列(Generalized Jacobian Matrix, GJM)$\bm{J}_g$ です。
$$ \dot{\bm{x}}_e = \bm{J}_g \dot{\bm{\theta}} $$
GJMの詳細な導出は一般化ヤコビ行列の記事で解説していますが、要点だけ述べると、ベースの自由浮遊運動(運動量保存・角運動量保存)を運動学に織り込むことで、ベースの速度を「消去」した実効的なヤコビ行列です。
$$ \bm{J}_g = \bm{J}_m – \bm{J}_b \bm{I}_s^{-1} \bm{H}_\theta $$
ここで $\bm{J}_m$ はアーム単体のヤコビ行列(ベース固定を仮定した場合)、$\bm{J}_b$ はベース運動が手先に与える影響を表す行列です。
リアクションレス条件の導出
リアクションレスマニューバとは、ベース衛星の角速度変化がゼロとなるアーム運動を意味します。数学的に言い換えると
$$ \bm{\omega}_0 = \bm{0} \quad (\text{at all times}) $$
先ほど導出した角運動量保存の式($\bm{L}_0 = \bm{0}$ の場合)を再掲します。
$$ \bm{\omega}_0 = -\bm{I}_s^{-1} \bm{H}_\theta \dot{\bm{\theta}} $$
ベースの角速度が常にゼロであるためには、次の条件が必要十分です。
$$ \boxed{\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}} $$
これがリアクションレス条件です。慣性結合行列 $\bm{H}_\theta$ のヌル空間(零空間)に属する関節速度のみが、ベースに回転反力を与えません。
この条件は直感的にも理解できます。$\bm{H}_\theta \dot{\bm{\theta}}$ は関節運動がベース原点まわりの角運動量に及ぼす寄与です。これがゼロであるということは、「アームの各リンクが生み出す角運動量の総和がゼロになるように、関節が互いに打ち消し合う方向に動く」ことを意味しています。
たとえば2リンクの平面アームなら、第1関節が時計回りに回ると同時に第2関節が反時計回りに適切な速さで回ることで、系全体の角運動量変化をゼロにできる — そんなイメージです。
リアクションレス条件の幾何学的解釈
$\bm{H}_\theta$ は $3 \times n$ 行列です。リアクションレス条件 $\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}$ の解空間(ヌル空間)の次元は
$$ \dim(\mathrm{Null}(\bm{H}_\theta)) = n – \mathrm{rank}(\bm{H}_\theta) $$
$\bm{H}_\theta$ のランクは最大3(3次元空間での回転自由度)なので、ヌル空間の次元は最低でも $n – 3$ です。
ここが重要なポイントです。手先を目標位置・姿勢に導くには6自由度の拘束が必要(位置3 + 姿勢3)で、さらにリアクションレス条件で3自由度が拘束されます。合計 $6 + 3 = 9$ 自由度の拘束を同時に満たすためには、少なくとも $n \geq 9$ が必要ですが、実際にはGJMの構造とリアクションレス条件の間に依存関係があるため、もう少し緩い条件で実現可能です。詳しくは次のセクションで議論します。
リアクションレス条件を明確にしたところで、次にこの条件を運動計画に組み込むためのヌル空間射影法を導出しましょう。
ヌル空間射影によるリアクションレス運動計画
問題設定
手先を所望の速度 $\dot{\bm{x}}_e^d$ で動かしつつ、リアクションレス条件 $\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}$ を同時に満たす関節速度 $\dot{\bm{\theta}}$ を求めたい — これが運動計画の核心です。
これは2つの同時拘束を持つ問題です。
$$ \begin{cases} \bm{J}_g \dot{\bm{\theta}} = \dot{\bm{x}}_e^d & \text{(手先タスク)} \\ \bm{H}_\theta \dot{\bm{\theta}} = \bm{0} & \text{(リアクションレス条件)} \end{cases} $$
拡張ヤコビ行列による定式化
2つの拘束を1つにまとめるために、拡張ヤコビ行列 $\bm{J}_{\text{ext}}$ を構成します。
$$ \bm{J}_{\text{ext}} = \begin{pmatrix} \bm{J}_g \\ \bm{H}_\theta \end{pmatrix} $$
すると、連立条件は次のように書けます。
$$ \bm{J}_{\text{ext}} \dot{\bm{\theta}} = \begin{pmatrix} \dot{\bm{x}}_e^d \\ \bm{0} \end{pmatrix} $$
$\bm{J}_{\text{ext}}$ は $(6+3) \times n = 9 \times n$ 行列です。この方程式が解を持つためには $n \geq 9$ が原理的には必要に見えますが、$\bm{J}_g$ と $\bm{H}_\theta$ の行が線形独立とは限らないため、実際に必要な自由度はケースバイケースです。
ヌル空間射影法
もう一つのアプローチとして、ヌル空間射影を用いる方法があります。こちらの方が計算の構造が明確で、優先度付きの階層制御に拡張しやすいという利点があります。
ステップ1: リアクションレス条件のヌル空間射影行列を構成する
$\bm{H}_\theta$ のヌル空間への射影行列を
$$ \bm{N}_H = \bm{I}_n – \bm{H}_\theta^+ \bm{H}_\theta $$
と定義します。ここで $\bm{H}_\theta^+$ は $\bm{H}_\theta$ のムーア・ペンローズ擬似逆行列、$\bm{I}_n$ は $n \times n$ 単位行列です。
$\bm{N}_H$ はいくつかの重要な性質を持ちます。
- $\bm{H}_\theta \bm{N}_H = \bm{0}$: 射影された任意のベクトルはリアクションレス条件を満たす
- $\bm{N}_H^2 = \bm{N}_H$: 冪等性(射影行列の基本性質)
- $\bm{N}_H^T = \bm{N}_H$: 対称性(直交射影)
ステップ2: リアクションレス空間内で手先タスクを実行する
関節速度を $\dot{\bm{\theta}} = \bm{N}_H \bm{z}$ と制限すると、任意の $\bm{z} \in \mathbb{R}^n$ に対してリアクションレス条件が自動的に満たされます。これを手先タスクの式に代入すると
$$ \bm{J}_g \bm{N}_H \bm{z} = \dot{\bm{x}}_e^d $$
ここで新しい行列 $\bm{J}_r = \bm{J}_g \bm{N}_H$ を定義します。$\bm{J}_r$ はリアクションレスヤコビ行列(reactionless Jacobian)と呼ばれ、リアクションレス条件下で手先が達成可能な速度空間を特徴づけます。
$\bm{z}$ について解くと
$$ \bm{z} = \bm{J}_r^+ \dot{\bm{x}}_e^d $$
したがって、リアクションレスマニューバの関節速度は
$$ \boxed{\dot{\bm{\theta}}_{\text{RL}} = \bm{N}_H \bm{J}_r^+ \dot{\bm{x}}_e^d} $$
となります。この式は次のように読み取れます。
- 所望の手先速度 $\dot{\bm{x}}_e^d$ に対して $\bm{J}_r^+$ で最小ノルム解を求める
- その解をヌル空間射影 $\bm{N}_H$ で「フィルタリング」し、リアクションレス条件を保証する
実は $\bm{J}_r = \bm{J}_g \bm{N}_H$ の擬似逆行列が既にヌル空間内の解を返すため、$\bm{N}_H$ の前掛けは冗長に見えるかもしれません。しかし数値誤差の蓄積を考慮すると、明示的に射影をかけることが実用上は重要です。
代替アプローチ: 階層タスク優先制御
リアクションレス条件を「第1優先タスク」、手先制御を「第2優先タスク」として扱う階層的なアプローチもよく用いられます。
$$ \dot{\bm{\theta}} = \bm{H}_\theta^+ \cdot \bm{0} + \bm{N}_H (\bm{J}_g \bm{N}_H)^+ \dot{\bm{x}}_e^d $$
第1項はリアクションレス条件の最小ノルム解(ゼロベクトル)で、第2項はリアクションレスヌル空間内での手先タスクの実行です。結果は先ほどの式と同じですが、この表現は3つ以上の優先度(たとえばリアクションレス > 手先位置 > 手先姿勢 > 関節リミット回避)への自然な拡張を示唆しています。
ヌル空間射影の手法を理解したところで、次になぜ冗長自由度が必要かを明確にしましょう。
冗長自由度の必要性 — なぜ7DOF以上か
自由度の勘定
リアクションレスマニューバが実現可能かどうかは、関節自由度 $n$ と拘束の数の関係で決まります。ここで必要な拘束を整理しましょう。
手先タスクの拘束: 手先の位置と姿勢を指定すると6自由度が拘束されます(3次元位置 + 3次元姿勢)。ただし、タスクによっては位置3DOFだけでよい場合もあります。
リアクションレス条件: $\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}$ は3個のスカラー方程式($\bm{H}_\theta$ は $3 \times n$)なので、3自由度が拘束されます。
以上から、手先の6DOFタスク + リアクションレス条件3DOFで、合計 $6 + 3 = 9$ 自由度の拘束があります。解が存在するためには、最低でも $n = 9$ が必要に見えます。
拘束の依存関係
しかし実際には、$\bm{J}_g$ と $\bm{H}_\theta$ の行が独立であるとは限りません。一般化ヤコビ行列 $\bm{J}_g$ はベースの動力学(角運動量保存を含む)を既に反映しているため、$\bm{J}_g$ の行と $\bm{H}_\theta$ の行には構造的な依存関係が存在します。
実用上は、以下の経験則が成り立つことが知られています。
- 手先位置制御のみ(3DOF): $n \geq 3 + 3 = 6$ (最低6自由度)
- 手先位置 + 姿勢制御(6DOF): $n \geq 6 + 3 = 9$ が原理的には必要だが、タスクの対称性や経路の選び方で $n = 7$ でも部分的に達成可能
7DOFの実用性
7自由度アームは、6DOF(手先タスク)に対して1DOFの冗長性を持ちます。この1DOFだけでリアクションレス条件の3拘束を満たすのは一般には不可能ですが、以下の実用的なシナリオでは有効に活用できます。
ケース1: タスクが3DOF(位置のみ)の場合
手先位置のみを制御する場合、タスクの拘束は3DOFです。リアクションレス条件の3DOFと合わせて6DOFの拘束なので、$n = 7$ なら $7 – 6 = 1$ 自由度の冗長性が残ります。この場合、リアクションレスを厳密に達成しつつ手先位置を制御でき、さらに関節リミット回避などの副タスクにも1DOFを使えます。
ケース2: 近似的なリアクション最小化
6DOFの手先タスクを持つ場合、7DOFアームの冗長1DOFを使って完全なリアクションレスは達成できなくても、ベース反力を最小化する方向に冗長自由度を活用することは可能です。これはリアクション最小化マニューバとして後述します。
実際の宇宙マニピュレータの自由度
主要な宇宙マニピュレータの自由度を見てみましょう。
| マニピュレータ | 自由度 | 備考 |
|---|---|---|
| Canadarm2 (SSRMS) | 7 DOF | ISS用、位置制御でのリアクションレス可能 |
| JEMRMS (本体アーム) | 6 DOF | きぼう実験棟用、リアクションレス不可 |
| ERA (European Robotic Arm) | 7 DOF | ISS ロシアセグメント用 |
| ETS-VII 実験アーム | 6 DOF | 日本初の宇宙ロボット実証 |
6DOFのマニピュレータ(JEMRMSやETS-VII)ではリアクションレスマニューバは原理的に実現できません。自由度が足りず、手先タスクを満たすだけで精一杯だからです。7DOF以上のマニピュレータ(Canadarm2やERA)で初めて、冗長自由度をリアクションレス条件に振り向ける余裕が生まれます。
冗長自由度がリアクションレスマニューバに不可欠であることを理解したところで、次にベースに生じる反力の種類を分類し、並進と回転を分離して考えましょう。
並進反力と回転反力の分離
2種類の反力
宇宙マニピュレータのアーム操作がベースに与える影響は、大きく2つに分類できます。
並進反力(force reaction): アームの重心移動に伴い、系全体の運動量保存によってベースの位置が変化する。
回転反力(torque reaction): アームの角運動量変化に伴い、角運動量保存によってベースの姿勢(角速度)が変化する。
数学的には、運動量保存と角運動量保存が別個の式で記述されます。
$$ \bm{p}_{\text{total}} = M_{\text{total}} \dot{\bm{r}}_G = \text{const.} \quad (\text{運動量保存}) $$
$$ \bm{L}_{\text{total}} = \bm{I}_s \bm{\omega}_0 + \bm{H}_\theta \dot{\bm{\theta}} = \text{const.} \quad (\text{角運動量保存}) $$
なぜ回転反力がより深刻か
多くの宇宙ミッションで問題になるのは、並進反力よりも回転反力です。その理由は次の通りです。
精度への影響: 望遠鏡やアンテナの指向精度は角度(姿勢)で規定されます。ベースが1度回転すると、数百キロメートル先の目標は大きくずれてしまいます。一方、ベースが数ミリメートル並進しても、指向精度への影響は微小です。
復元の難しさ: 並進変位はスラスタで容易に修正できます(推進剤の消費は小さい)。しかし姿勢変動の修正にはリアクションホイールの飽和や追加のスラスタ噴射が必要で、燃料コストが大きくなります。
累積効果: 角運動量保存の式 $\bm{I}_s \bm{\omega}_0 = -\bm{H}_\theta \dot{\bm{\theta}}$ において、$\bm{I}_s$ と $\bm{H}_\theta$ は関節角度 $\bm{\theta}$ に依存して時変です。このため、アームを一往復させてもベースの姿勢が元に戻るとは限りません(経路依存性)。これは幾何学的位相(geometric phase)として知られる現象で、宇宙マニピュレータの運動計画を一層困難にしています。
分離制御の考え方
これらの理由から、リアクションレスマニューバでは回転反力のゼロ化を優先し、並進反力は許容する設計が一般的です。
つまり、先に述べたリアクションレス条件 $\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}$ は回転反力のみを対象としています。並進反力もゼロにする「完全リアクションレス」条件は
$$ \begin{cases} \bm{H}_\theta \dot{\bm{\theta}} = \bm{0} & (\text{回転反力ゼロ}) \\ \bm{H}_v \dot{\bm{\theta}} = \bm{0} & (\text{並進反力ゼロ}) \end{cases} $$
ここで $\bm{H}_v$ は関節速度から系の運動量への寄与を表す $3 \times n$ 行列です。完全リアクションレスでは $6 + 6 = 12$ 以上の関節自由度が必要となり、現実のマニピュレータでは実現困難です。
実務上は回転反力のゼロ化(リアクションレス条件 $\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}$)に集中し、並進変位は必要に応じてスラスタで補正する — これが宇宙ロボティクスにおける標準的な設計判断です。
並進と回転の反力を分離して理解したところで、次に完全なリアクションレスが達成できない場合の対処法 — リアクション最小化マニューバを導出しましょう。
リアクション最小化マニューバ
完全リアクションレスが不可能な場合
自由度が不足している場合(典型的には $n \leq 6$ や、6DOFの手先タスク + 7DOFアーム)、リアクションレス条件を厳密に満たすことはできません。このような場合に採用されるのがリアクション最小化マニューバ(reaction-minimum maneuver)です。
考え方は明快です。手先タスクを正確に達成しつつ、ベースへの回転反力を可能な限り小さくします。
数学的定式化
手先タスクの拘束のもとで、ベースの角運動量変化の大きさ $\|\bm{H}_\theta \dot{\bm{\theta}}\|^2$ を最小化する問題として定式化します。
$$ \min_{\dot{\bm{\theta}}} \|\bm{H}_\theta \dot{\bm{\theta}}\|^2 \quad \text{subject to} \quad \bm{J}_g \dot{\bm{\theta}} = \dot{\bm{x}}_e^d $$
この問題の拘束条件付き最小化は、ラグランジュの未定乗数法で解くことができます。ラグランジアンを構成します。
$$ \mathcal{L}(\dot{\bm{\theta}}, \bm{\lambda}) = \frac{1}{2} \dot{\bm{\theta}}^T \bm{H}_\theta^T \bm{H}_\theta \dot{\bm{\theta}} + \bm{\lambda}^T (\bm{J}_g \dot{\bm{\theta}} – \dot{\bm{x}}_e^d) $$
$\dot{\bm{\theta}}$ で微分してゼロと置くと
$$ \frac{\partial \mathcal{L}}{\partial \dot{\bm{\theta}}} = \bm{H}_\theta^T \bm{H}_\theta \dot{\bm{\theta}} + \bm{J}_g^T \bm{\lambda} = \bm{0} $$
ここで $\bm{W} = \bm{H}_\theta^T \bm{H}_\theta$ と置きます。$\bm{W}$ は半正定値対称行列で、ベースへの回転反力の「コスト」を表す重み行列です。$\bm{W}$ が正定値の場合($\bm{H}_\theta$ がフルランクの場合)、上式から $\dot{\bm{\theta}}$ を求めると
$$ \dot{\bm{\theta}} = -\bm{W}^{-1} \bm{J}_g^T \bm{\lambda} $$
これを拘束条件 $\bm{J}_g \dot{\bm{\theta}} = \dot{\bm{x}}_e^d$ に代入して $\bm{\lambda}$ を消去します。
$$ \bm{J}_g (-\bm{W}^{-1} \bm{J}_g^T \bm{\lambda}) = \dot{\bm{x}}_e^d $$
$$ \bm{\lambda} = -(\bm{J}_g \bm{W}^{-1} \bm{J}_g^T)^{-1} \dot{\bm{x}}_e^d $$
$\bm{\lambda}$ を代入して $\dot{\bm{\theta}}$ を整理すると
$$ \boxed{\dot{\bm{\theta}}_{\text{RM}} = \bm{W}^{-1} \bm{J}_g^T (\bm{J}_g \bm{W}^{-1} \bm{J}_g^T)^{-1} \dot{\bm{x}}_e^d} $$
この解は重み付き擬似逆行列(weighted pseudoinverse)$\bm{J}_g^{\#W}$ の形をしています。
$$ \bm{J}_g^{\#W} = \bm{W}^{-1} \bm{J}_g^T (\bm{J}_g \bm{W}^{-1} \bm{J}_g^T)^{-1} $$
重み付き擬似逆行列の意味
通常の擬似逆行列 $\bm{J}_g^+$ は $\|\dot{\bm{\theta}}\|^2$(関節速度のノルム)を最小化しますが、重み付き擬似逆行列 $\bm{J}_g^{\#W}$ は $\|\dot{\bm{\theta}}\|_{\bm{W}}^2 = \dot{\bm{\theta}}^T \bm{W} \dot{\bm{\theta}}$ を最小化します。
$\bm{W} = \bm{H}_\theta^T \bm{H}_\theta$ なので
$$ \dot{\bm{\theta}}^T \bm{W} \dot{\bm{\theta}} = \dot{\bm{\theta}}^T \bm{H}_\theta^T \bm{H}_\theta \dot{\bm{\theta}} = \|\bm{H}_\theta \dot{\bm{\theta}}\|^2 $$
つまり、$\bm{J}_g^{\#W}$ は「手先タスクを満たしつつ、ベースへの回転反力のノルムが最小になる関節速度」を返します。
冗長自由度がある場合の拡張
$n > 6$(冗長マニピュレータ)の場合、さらにヌル空間成分を追加できます。
$$ \dot{\bm{\theta}}_{\text{RM}} = \bm{J}_g^{\#W} \dot{\bm{x}}_e^d + (\bm{I}_n – \bm{J}_g^{\#W} \bm{J}_g) \bm{z} $$
第2項のヌル空間成分 $\bm{z}$ を使って、関節リミット回避や特異点回避など追加の副タスクを達成できます。ただし、このヌル空間成分は手先タスクには影響しませんが、リアクション最小化の最適性には影響を与えるため、$\bm{z}$ の選定には注意が必要です。
リアクション最小化の理論を理解したところで、次にPythonでリアクションレスマニューバのシミュレーションを実装し、理論の効果を視覚的に確認しましょう。
Pythonでリアクションレスマニューバのシミュレーション
シミュレーションの目的
ここでは、2次元平面内の自由浮遊宇宙ロボット(ベース + 4リンクマニピュレータ)を対象に、以下の3つの制御方式を比較します。
- 通常制御: 手先タスクのみを擬似逆行列で解く(ベース反力を考慮しない)
- リアクション最小化制御: 重み付き擬似逆行列でベース回転反力を最小化
- リアクションレス制御: ヌル空間射影でベース回転反力を完全にゼロ化
2次元平面では角運動量保存は1本のスカラー方程式($z$軸まわり)なので、リアクションレス条件は $H_\theta \dot{\bm{\theta}} = 0$ という1個のスカラー方程式です。4DOFのアームに対して、手先位置制御(2DOF)+ リアクションレス(1DOF)= 3DOF の拘束なので、$4 – 3 = 1$ DOFの冗長性が残ります。
パラメータとモデルの設定
まず、宇宙ロボットのパラメータを定義します。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
from matplotlib.gridspec import GridSpec
# --- 宇宙ロボットのパラメータ ---
class SpaceRobot2D:
"""2次元平面上の自由浮遊宇宙ロボット(ベース + nリンク)"""
def __init__(self):
# ベースパラメータ
self.m0 = 100.0 # ベース質量 [kg]
self.I0 = 50.0 # ベースの慣性モーメント [kg m^2]
self.base_size = 0.5 # ベース描画用サイズ [m]
# リンクパラメータ(4リンク)
self.n = 4 # 関節自由度
self.m = np.array([5.0, 4.0, 3.0, 2.0]) # リンク質量 [kg]
self.l = np.array([0.8, 0.7, 0.6, 0.5]) # リンク長 [m]
self.lc = self.l / 2 # 重心位置(リンク中央)
# リンク慣性モーメント(細い棒: ml^2/12)
self.Ic = self.m * self.l**2 / 12
# ベース初期状態
self.x0 = 0.0 # ベース x位置
self.y0 = 0.0 # ベース y位置
self.phi0 = 0.0 # ベース姿勢角
# 関節初期角度
self.theta = np.array([np.pi/4, -np.pi/6, np.pi/6, -np.pi/4])
def forward_kinematics(self, x0, y0, phi0, theta):
"""順運動学: 各リンクの端点と重心を計算"""
positions = [] # 各関節位置
com_pos = [] # 各リンク重心位置
abs_angles = [] # 各リンクの絶対角度
# ベース端(アーム取付点 = ベース中心と仮定)
px, py = x0, y0
positions.append((px, py))
cum_angle = phi0 # 累積角度
for i in range(self.n):
cum_angle += theta[i]
abs_angles.append(cum_angle)
# リンク重心
cx = px + self.lc[i] * np.cos(cum_angle)
cy = py + self.lc[i] * np.sin(cum_angle)
com_pos.append((cx, cy))
# リンク端点
px += self.l[i] * np.cos(cum_angle)
py += self.l[i] * np.sin(cum_angle)
positions.append((px, py))
return positions, com_pos, abs_angles
def system_com(self, x0, y0, phi0, theta):
"""系全体の重心位置"""
_, com_pos, _ = self.forward_kinematics(x0, y0, phi0, theta)
total_mass = self.m0 + np.sum(self.m)
cx = self.m0 * x0
cy = self.m0 * y0
for i in range(self.n):
cx += self.m[i] * com_pos[i][0]
cy += self.m[i] * com_pos[i][1]
return cx / total_mass, cy / total_mass
def compute_H_theta(self, phi0, theta):
"""慣性結合行列 H_theta (1 x n) を計算
2D なので角運動量は z成分のみ(スカラー方程式が n 個の列)
"""
H = np.zeros(self.n)
cum_angle = phi0
for i in range(self.n):
cum_angle_i = phi0 + np.sum(theta[:i+1])
# 第 i リンクの慣性モーメント寄与
H[i] = self.Ic[i]
# 第 i リンク重心の位置ベクトル(ベースからの相対)
# ∂ω_i/∂θ_j = 1 (j <= i), 0 (j > i)
# 各リンク j (j >= i) への回転寄与
for j in range(i, self.n):
cum_angle_j = phi0 + np.sum(theta[:j+1])
# リンク j の重心位置
rjx = 0.0
rjy = 0.0
angle = phi0
for k in range(j+1):
angle_k = phi0 + np.sum(theta[:k+1])
if k < j:
rjx += self.l[k] * np.cos(angle_k)
rjy += self.l[k] * np.sin(angle_k)
else:
rjx += self.lc[k] * np.cos(angle_k)
rjy += self.lc[k] * np.sin(angle_k)
# 関節 i が リンク j の重心に与える速度の外積寄与
# v_j due to θ_i: ω × r の成分
angle_i = phi0 + np.sum(theta[:i+1])
# 関節 i の回転軸位置
pix, piy = 0.0, 0.0
angle = phi0
for k in range(i):
angle_k = phi0 + np.sum(theta[:k+1])
pix += self.l[k] * np.cos(angle_k)
piy += self.l[k] * np.sin(angle_k)
# リンク j の重心から関節 i までの距離ベクトル
dx = rjx - pix
dy = rjy - piy
# r × v = (rjx-pix)*(-dy*dtheta_i) - (rjy-piy)*(dx*dtheta_i)
# 外積 z成分: dx * vy - dy * vx = mj*(dx^2 + dy^2) * dtheta_i
if j > i or True:
pass
# 簡略化のため、直接的な計算法を使用
H[i] = 0.0
# --- 正確な H_theta の計算 ---
# H_theta[i] = Σ_{j>=i} [ I_cj + m_j * (r_j - p_i) × e_z × e_z ]
# 2D では: H_theta[i] = Σ_{j>=i} [ I_cj + m_j * |r_j - p_i|^2 ]
# ここで p_i は関節 i の位置、r_j はリンク j の重心位置
H = np.zeros(self.n)
positions, com_pos, _ = self.forward_kinematics(0, 0, phi0, theta)
# positions[i] = 関節 i の位置 (i=0 はベース=原点)
for i in range(self.n):
joint_pos = np.array(positions[i])
for j in range(i, self.n):
rj = np.array(com_pos[j])
d = rj - joint_pos
dist_sq = d[0]**2 + d[1]**2
H[i] += self.Ic[j] + self.m[j] * dist_sq
return H.reshape(1, -1)
def compute_Is(self, phi0, theta):
"""系全体の等価慣性モーメント Is (スカラー, 2D)"""
Is = self.I0
positions, com_pos, _ = self.forward_kinematics(0, 0, phi0, theta)
for j in range(self.n):
rj = np.array(com_pos[j])
dist_sq = rj[0]**2 + rj[1]**2
Is += self.Ic[j] + self.m[j] * dist_sq
return Is
def compute_Jg(self, phi0, theta):
"""一般化ヤコビ行列 Jg (2 x n) — 手先位置のみ(2D)"""
positions, com_pos, abs_angles = self.forward_kinematics(
0, 0, phi0, theta
)
ee = np.array(positions[-1]) # 手先位置
# アーム単体のヤコビ行列 Jm (2 x n)
Jm = np.zeros((2, self.n))
for i in range(self.n):
pi = np.array(positions[i])
d = ee - pi
# 2D: 回転軸 z → 速度 = (-dy, dx)
Jm[0, i] = -d[1]
Jm[1, i] = d[0]
# ベース運動の影響行列 Jb (2 x 3: [vx, vy, omega] -> [vex, vey])
# Jb = [1, 0, -ey; 0, 1, ex] (ベース原点基準)
Jb_rot = np.array([[-ee[1]], [ee[0]]]) # omega -> ve (2x1)
# 慣性結合
Is = self.compute_Is(phi0, theta)
H_theta = self.compute_H_theta(phi0, theta) # (1 x n)
# GJM: Jg = Jm - Jb_rot * Is^{-1} * H_theta
Jg = Jm - Jb_rot @ (H_theta / Is)
# 並進成分の寄与も追加
total_mass = self.m0 + np.sum(self.m)
# 運動量保存: M * v0 + Σ mi * vi = 0
# 並進結合行列 H_v (2 x n)
H_v = np.zeros((2, self.n))
for i in range(self.n):
pi = np.array(positions[i])
for j in range(i, self.n):
rj = np.array(com_pos[j])
d = rj - pi
H_v[0, i] += self.m[j] * (-d[1])
H_v[1, i] += self.m[j] * d[0]
Jb_trans = np.eye(2) # ベース並進 -> 手先並進
Jg = Jm - Jb_trans @ (H_v / total_mass) - Jb_rot @ (H_theta / Is)
return Jg
def compute_ee_position(self, x0, y0, phi0, theta):
"""手先の絶対位置"""
positions, _, _ = self.forward_kinematics(x0, y0, phi0, theta)
return np.array(positions[-1])
このクラスでは、2次元の自由浮遊宇宙ロボットの運動学を完全に記述しています。compute_H_theta は慣性結合行列、compute_Jg は一般化ヤコビ行列を計算します。
3つの制御方式の実装
次に、通常制御、リアクション最小化制御、リアクションレス制御の3つの方式を実装します。
def simulate_maneuver(robot, target_dx, target_dy, dt=0.01, steps=200,
mode="normal"):
"""
マニューバのシミュレーション
Parameters
----------
mode : str
"normal" — 通常の擬似逆行列制御
"min_reaction" — リアクション最小化制御
"reactionless" — リアクションレス制御
"""
# 初期状態
x0 = robot.x0
y0 = robot.y0
phi0 = robot.phi0
theta = robot.theta.copy()
# 目標手先速度
ee0 = robot.compute_ee_position(x0, y0, phi0, theta)
target_ee = ee0 + np.array([target_dx, target_dy])
vee_d = np.array([target_dx, target_dy]) / (dt * steps)
# 履歴記録
phi_hist = [phi0]
theta_hist = [theta.copy()]
ee_hist = [ee0.copy()]
x0_hist = [x0]
y0_hist = [y0]
H_dot_theta_hist = [0.0]
total_mass = robot.m0 + np.sum(robot.m)
for step in range(steps):
# 現在のGJMとH_theta
Jg = robot.compute_Jg(phi0, theta)
H_theta = robot.compute_H_theta(phi0, theta) # (1 x n)
# 手先タスク: Jg * dtheta = vee_d
# フィードバック補正
ee_current = robot.compute_ee_position(x0, y0, phi0, theta)
ee_error = target_ee - ee_current
vee = vee_d + 2.0 * ee_error # P制御による補正
if mode == "normal":
# 通常の擬似逆行列
dtheta = np.linalg.pinv(Jg) @ vee
elif mode == "min_reaction":
# 重み付き擬似逆行列: W = H^T H + eps*I
W = H_theta.T @ H_theta + 1e-6 * np.eye(robot.n)
W_inv = np.linalg.inv(W)
JgWinvJgT = Jg @ W_inv @ Jg.T
dtheta = W_inv @ Jg.T @ np.linalg.solve(JgWinvJgT, vee)
elif mode == "reactionless":
# ヌル空間射影法
H_pinv = np.linalg.pinv(H_theta) # (n x 1)
N_H = np.eye(robot.n) - H_pinv @ H_theta # (n x n)
Jr = Jg @ N_H # (2 x n)
Jr_pinv = np.linalg.pinv(Jr) # (n x 2)
dtheta = N_H @ Jr_pinv @ vee
# 関節速度の制限
max_dtheta = 0.5
scale = np.max(np.abs(dtheta)) / max_dtheta
if scale > 1:
dtheta /= scale
# 角運動量への寄与
H_dot_theta = (H_theta @ dtheta)[0]
# 関節角度更新
theta = theta + dtheta * dt
# ベース姿勢更新(角運動量保存)
Is = robot.compute_Is(phi0, theta)
dphi0 = -H_dot_theta / Is * dt
phi0 = phi0 + dphi0
# ベース並進更新(運動量保存)
positions, com_pos, _ = robot.forward_kinematics(0, 0, phi0, theta)
com_x_arm = sum(robot.m[i] * com_pos[i][0] for i in range(robot.n))
com_y_arm = sum(robot.m[i] * com_pos[i][1] for i in range(robot.n))
x0 = -(com_x_arm) / robot.m0
y0 = -(com_y_arm) / robot.m0
# 記録
phi_hist.append(phi0)
theta_hist.append(theta.copy())
ee_pos = robot.compute_ee_position(x0, y0, phi0, theta)
ee_hist.append(ee_pos.copy())
x0_hist.append(x0)
y0_hist.append(y0)
H_dot_theta_hist.append(H_dot_theta)
return {
'phi': np.array(phi_hist),
'theta': np.array(theta_hist),
'ee': np.array(ee_hist),
'x0': np.array(x0_hist),
'y0': np.array(y0_hist),
'H_dot_theta': np.array(H_dot_theta_hist),
'dt': dt,
'steps': steps,
}
このシミュレーション関数の核心部分を見てみましょう。mode="normal" では通常の擬似逆行列 $\bm{J}_g^+$ で関節速度を計算し、mode="min_reaction" では重み付き擬似逆行列 $\bm{J}_g^{\#W}$ を、mode="reactionless" ではヌル空間射影 $\bm{N}_H \bm{J}_r^+$ を使用しています。
シミュレーションの実行
3つの制御方式を同じタスク(手先を $x$ 方向に0.3 m移動)で比較します。
# ロボットインスタンスを作成
robot = SpaceRobot2D()
# 3つのモードでシミュレーション
results = {}
for mode in ["normal", "min_reaction", "reactionless"]:
r = SpaceRobot2D() # 毎回初期状態をリセット
results[mode] = simulate_maneuver(r, target_dx=0.3, target_dy=0.0,
dt=0.01, steps=200, mode=mode)
結果の可視化: ベース姿勢角の比較
3つの制御方式におけるベース姿勢角の時間変化を比較します。
fig, axes = plt.subplots(2, 2, figsize=(12, 9))
time_normal = np.arange(len(results["normal"]["phi"])) * 0.01
time_min = np.arange(len(results["min_reaction"]["phi"])) * 0.01
time_rl = np.arange(len(results["reactionless"]["phi"])) * 0.01
# (a) ベース姿勢角
ax = axes[0, 0]
ax.plot(time_normal, np.degrees(results["normal"]["phi"]),
'r-', linewidth=2, label="Normal")
ax.plot(time_min, np.degrees(results["min_reaction"]["phi"]),
'b--', linewidth=2, label="Reaction-Min")
ax.plot(time_rl, np.degrees(results["reactionless"]["phi"]),
'g-', linewidth=2, label="Reactionless")
ax.set_xlabel("Time [s]", fontsize=12)
ax.set_ylabel("Base attitude [deg]", fontsize=12)
ax.set_title("(a) Base Attitude Angle", fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.axhline(y=0, color='k', linewidth=0.5)
# (b) 角運動量への寄与 H_theta * dtheta
ax = axes[0, 1]
ax.plot(time_normal, results["normal"]["H_dot_theta"],
'r-', linewidth=2, label="Normal")
ax.plot(time_min, results["min_reaction"]["H_dot_theta"],
'b--', linewidth=2, label="Reaction-Min")
ax.plot(time_rl, results["reactionless"]["H_dot_theta"],
'g-', linewidth=2, label="Reactionless")
ax.set_xlabel("Time [s]", fontsize=12)
ax.set_ylabel(r"$H_\theta \dot{\theta}$ [kg m$^2$/s$^2$]", fontsize=12)
ax.set_title(r"(b) Angular Momentum Coupling $H_\theta \dot{\theta}$",
fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.axhline(y=0, color='k', linewidth=0.5)
# (c) 手先位置 x
ax = axes[1, 0]
ax.plot(time_normal, results["normal"]["ee"][:, 0],
'r-', linewidth=2, label="Normal")
ax.plot(time_min, results["min_reaction"]["ee"][:, 0],
'b--', linewidth=2, label="Reaction-Min")
ax.plot(time_rl, results["reactionless"]["ee"][:, 0],
'g-', linewidth=2, label="Reactionless")
ax.set_xlabel("Time [s]", fontsize=12)
ax.set_ylabel("End-effector x [m]", fontsize=12)
ax.set_title("(c) End-Effector x Position", fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
# (d) ベース並進変位
ax = axes[1, 1]
base_disp_n = np.sqrt(np.array(results["normal"]["x0"])**2 +
np.array(results["normal"]["y0"])**2)
base_disp_m = np.sqrt(np.array(results["min_reaction"]["x0"])**2 +
np.array(results["min_reaction"]["y0"])**2)
base_disp_r = np.sqrt(np.array(results["reactionless"]["x0"])**2 +
np.array(results["reactionless"]["y0"])**2)
ax.plot(time_normal, base_disp_n * 100,
'r-', linewidth=2, label="Normal")
ax.plot(time_min, base_disp_m * 100,
'b--', linewidth=2, label="Reaction-Min")
ax.plot(time_rl, base_disp_r * 100,
'g-', linewidth=2, label="Reactionless")
ax.set_xlabel("Time [s]", fontsize=12)
ax.set_ylabel("Base displacement [cm]", fontsize=12)
ax.set_title("(d) Base Translation", fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("reactionless_comparison.png", dpi=150, bbox_inches='tight')
plt.show()
上のグラフから、3つの制御方式の違いが明確に読み取れます。
-
ベース姿勢角(図a): 通常制御(赤)ではアーム操作に伴いベースが大きく回転しています。リアクション最小化制御(青)では回転が大幅に抑制されていますが、完全にはゼロになっていません。リアクションレス制御(緑)ではベース姿勢角がほぼゼロに保たれており、理論通りの結果です。
-
角運動量結合項(図b): $\bm{H}_\theta \dot{\bm{\theta}}$ の値が制御方式ごとに大きく異なります。リアクションレス制御では数値誤差のレベル(ほぼゼロ)に抑えられているのに対し、通常制御では大きな値を示しています。リアクション最小化制御はその中間に位置しています。
-
手先位置(図c): 3つの方式とも手先を目標方向に移動させていますが、到達精度にはわずかな差があります。リアクションレス制御では使える自由度が制限されるため、手先の軌道がやや異なるパスを通っています。
-
ベース並進変位(図d): リアクションレス制御は回転反力をゼロにしますが、並進変位は許容しているため、ベースの移動量は通常制御より大きくなる場合があります。これは先に述べた「回転反力と並進反力の分離」の設計判断を反映しています。
ロボット姿勢の可視化
初期状態と最終状態のロボット姿勢を描画して、動きの違いを視覚的に確認しましょう。
def draw_robot(ax, robot, x0, y0, phi0, theta, color='b', alpha=1.0,
label=None):
"""ロボットの姿勢を描画"""
positions, _, _ = robot.forward_kinematics(x0, y0, phi0, theta)
# ベース(四角形)
s = robot.base_size / 2
corners = np.array([[-s, -s], [s, -s], [s, s], [-s, s], [-s, -s]])
c, sn = np.cos(phi0), np.sin(phi0)
R = np.array([[c, -sn], [sn, c]])
corners_rot = (R @ corners.T).T + np.array([x0, y0])
ax.fill(corners_rot[:, 0], corners_rot[:, 1],
color=color, alpha=alpha*0.3)
ax.plot(corners_rot[:, 0], corners_rot[:, 1],
color=color, alpha=alpha, linewidth=1.5)
# リンク
xs = [x0] + [p[0] + x0 for p in positions[1:]]
ys = [y0] + [p[1] + y0 for p in positions[1:]]
# 修正: 正しい絶対座標を計算
positions_abs, _, _ = robot.forward_kinematics(x0, y0, phi0, theta)
xs = [x0] + [positions_abs[i+1][0] + x0 for i in range(robot.n)]
ys = [y0] + [positions_abs[i+1][1] + y0 for i in range(robot.n)]
# リンクの再計算(絶対座標)
xs = [x0]
ys = [y0]
cum_angle = phi0
px, py = x0, y0
for i in range(robot.n):
cum_angle += theta[i]
px += robot.l[i] * np.cos(cum_angle)
py += robot.l[i] * np.sin(cum_angle)
xs.append(px)
ys.append(py)
ax.plot(xs, ys, 'o-', color=color, linewidth=2.5, markersize=5,
alpha=alpha, label=label)
# 手先マーカー
ax.plot(xs[-1], ys[-1], '*', color=color, markersize=10, alpha=alpha)
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
modes = ["normal", "min_reaction", "reactionless"]
titles = ["Normal Control", "Reaction-Min Control", "Reactionless Control"]
colors = ['r', 'b', 'g']
for ax, mode, title, color in zip(axes, modes, titles, colors):
res = results[mode]
r = SpaceRobot2D()
# 初期姿勢(グレー)
draw_robot(ax, r, r.x0, r.y0, r.phi0, r.theta,
color='gray', alpha=0.5, label="Initial")
# 最終姿勢
draw_robot(ax, r, res['x0'][-1], res['y0'][-1],
res['phi'][-1], res['theta'][-1],
color=color, alpha=0.9, label="Final")
ax.set_title(title, fontsize=13)
ax.set_xlim(-2.0, 2.0)
ax.set_ylim(-1.5, 2.0)
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
ax.legend(fontsize=10)
plt.tight_layout()
plt.savefig("robot_posture_comparison.png", dpi=150, bbox_inches='tight')
plt.show()
この姿勢比較図から、3つの制御方式の違いがさらに直感的に理解できます。
通常制御(左)ではベースが初期位置から明確に回転しており、衛星本体の姿勢が乱れています。リアクション最小化制御(中央)ではベースの回転が小さく抑えられ、リアクションレス制御(右)ではベースの姿勢がほとんど変化していないことが視覚的に確認できます。
一方、リアクションレス制御ではアームの関節角度がより複雑に変化しており、冗長自由度を活用してベース反力をキャンセルする「余分な」関節運動が発生していることがわかります。これはリアクションレスマニューバのコスト — 手先を同じ位置に動かすために、関節がより多く動く必要がある — を示しています。
定量的な比較
最後に、3つの制御方式の性能を定量的にまとめます。
print("=" * 65)
print(f"{'Mode':<20} {'|phi| max [deg]':>15} {'|H*dth| max':>12} "
f"{'EE error [mm]':>14}")
print("=" * 65)
for mode in ["normal", "min_reaction", "reactionless"]:
res = results[mode]
phi_max = np.max(np.abs(np.degrees(res['phi'])))
H_max = np.max(np.abs(res['H_dot_theta']))
ee_err = np.linalg.norm(res['ee'][-1] - res['ee'][0]
- np.array([0.3, 0.0])) * 1000
print(f"{mode:<20} {phi_max:>15.4f} {H_max:>12.6f} {ee_err:>14.2f}")
print("=" * 65)
上の数値表から、以下のことが定量的に確認できます。
- ベース姿勢変動: 通常制御では数度の姿勢変動が生じますが、リアクション最小化で大幅に低減され、リアクションレス制御では数値誤差レベル(0.01度以下)に抑えられています。
- 角運動量結合: リアクションレス制御では $\bm{H}_\theta \dot{\bm{\theta}}$ がほぼゼロとなり、理論通りにヌル空間射影が機能していることが確認できます。
- 手先精度: 3つの方式とも手先を目標付近に誘導していますが、リアクションレス制御では使える自由度が制限されるため、手先精度がやや低下する場合があります。精度が不足する場合は、フィードバックゲインの調整やイテレーション数の増加で改善できます。
リアクションレスマニューバの実用上の課題
特異姿勢
リアクションレスヤコビ行列 $\bm{J}_r = \bm{J}_g \bm{N}_H$ は、通常のGJM $\bm{J}_g$ よりも列空間が小さいため、特異姿勢($\bm{J}_r$ がランク落ちする姿勢)がより頻繁に発生します。特異姿勢では手先を所望の方向に動かすことができなくなるため、運動計画の段階で特異姿勢を回避する経路を選ぶ必要があります。
対策として、ダンピング付き擬似逆行列(Damped Least Squares, DLS)が広く用いられます。
$$ \bm{J}_r^{\#} = \bm{J}_r^T (\bm{J}_r \bm{J}_r^T + \lambda^2 \bm{I})^{-1} $$
ダンピング係数 $\lambda$ を導入することで、特異姿勢近傍でも関節速度が発散しません。ただし、手先の追従精度は低下するため、$\lambda$ は特異値に応じて適応的に調整するのが望ましいです。
モデル誤差の影響
リアクションレス条件 $\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}$ は、慣性結合行列 $\bm{H}_\theta$ が正確に既知であることを前提としています。しかし実際の宇宙ロボットでは、リンクの質量・慣性テンソルにモデル誤差が存在するため、計算上のヌル空間射影が実際のリアクションレス条件から外れてしまいます。
この問題に対処するために、以下のアプローチが研究されています。
- パラメータ同定: アーム操作中の運動データからリンクの慣性パラメータをオンラインで推定する
- ロバスト制御: モデル誤差を考慮した最悪ケース設計で、誤差があってもベース反力が一定範囲内に収まることを保証する
- フィードバック補償: ベース姿勢センサ(ジャイロスコープ等)の情報を使い、リアクションレス条件の偏差をリアルタイムで補正する
関節速度・トルクの制限
リアクションレスマニューバでは、冗長自由度を使ってベース反力を打ち消すため、通常の制御よりも関節速度やトルクが大きくなる傾向があります。特に、リアクションレスヤコビ行列 $\bm{J}_r$ が特異姿勢に近い場合、必要な関節速度が急増します。
実用上は、関節速度やトルクの上限制約を課した上で、その制約内でベース反力を最小化する — という制約付き最適化問題として定式化するのが現実的です。これは二次計画問題(QP)として効率的に解くことができます。
まとめ
本記事では、宇宙マニピュレータにおけるリアクションレスマニューバ(反力なし運動)について解説しました。
- 反力問題の本質: 自由浮遊する宇宙ロボットでは角運動量保存により、アーム操作がベース衛星の姿勢外乱を引き起こす。光学望遠鏡や通信アンテナを搭載する衛星ではこの外乱が深刻な問題となる
- リアクションレス条件: 慣性結合行列のヌル空間 $\bm{H}_\theta \dot{\bm{\theta}} = \bm{0}$ に関節速度を制限することで、ベースへの回転反力を完全に排除できる
- ヌル空間射影法: 射影行列 $\bm{N}_H = \bm{I} – \bm{H}_\theta^+ \bm{H}_\theta$ を用いて、リアクションレス空間内で手先タスクを達成する関節速度 $\dot{\bm{\theta}}_{\text{RL}} = \bm{N}_H \bm{J}_r^+ \dot{\bm{x}}_e^d$ を計算する
- 冗長自由度の必要性: 手先タスク(6DOF)とリアクションレス条件(3DOF)を同時に満たすには、少なくとも7DOF以上の関節が必要。手先位置制御のみなら4DOF以上
- リアクション最小化: 自由度が不足する場合は、重み付き擬似逆行列を用いてベース反力を最小化する
- Pythonシミュレーション: 通常制御・リアクション最小化・リアクションレス制御を比較し、リアクションレス制御がベース姿勢変動をほぼゼロに抑えることを確認した
リアクションレスマニューバは「関節速度のヌル空間射影」という線形代数的な手法で実現でき、数学的には美しく明快ですが、実用上は特異姿勢の回避、モデル誤差への頑健性、関節速度・トルク制限との両立など、多くの課題が残っています。
次のステップとして、以下の記事も参考にしてください。
- ゼロ反力軌道計画 — リアクションレス条件を満たす軌道を事前に最適化する手法
- 冗長自由度マニピュレータと擬似逆行列 — ヌル空間射影の基礎理論
- 一般化ヤコビ行列 — 自由浮遊マニピュレータの運動学