作業空間解析とリーチャビリティ — ロボットアームが届く範囲を知る

国際宇宙ステーション(ISS)の外壁に太陽電池パネルを取り付ける作業を想像してください。宇宙飛行士が船外活動(EVA)で行うのは危険で時間もかかるため、全長 17.6 m のロボットアーム Canadarm2 がこの作業を担っています。しかし、ここで本質的な疑問が生じます — このアームは、本当に目的のパネル取り付け位置まで届くのでしょうか?

地上であれば人間が手を伸ばして確認できますが、宇宙空間では設計段階で「ロボットアームがどこに届き、どこに届かないか」を正確に把握しておかなければなりません。打ち上げ後に「あと 10 cm 届かない」では取り返しがつかないのです。この「届く範囲」を定量的に解析するのが作業空間解析(workspace analysis)であり、到達能力を方向まで含めて評価するのがリーチャビリティ(reachability)です。

作業空間解析を理解すると、以下のような応用が開けます。

  • 宇宙ロボットの設計: ISS の Canadarm2 や月面探査ローバーのアーム設計において、リンク長や関節配置を最適化する
  • 産業用ロボットのセル設計: 工場のロボットセルで、アームが全てのワークピースに到達できるかをレイアウト段階で検証する
  • 手術支援ロボット: 手術ロボットのアームが体内の目標部位に到達可能か、かつ十分な器用さ(任意の方向からアプローチできるか)を確認する
  • パワードスーツ・義肢: 装着者の自然な動きに追従するために必要な関節可動域を決定する

本記事の内容

  • 作業空間の直感的理解と分類(到達可能作業空間と器用作業空間)
  • 2リンクアームの解析的方法(幾何学的アプローチ)
  • モンテカルロサンプリングによる数値的方法
  • 関節リミットと障害物の影響
  • リーチャビリティマップによる到達能力の可視化
  • Python での 2D/3D 作業空間の実装と考察

前提知識

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

順運動学の知識があることを前提に進めます。順運動学とは、各関節の角度 $\bm{\theta} = (\theta_1, \theta_2, \dots, \theta_n)$ を入力として、手先(エンドエフェクタ)の位置・姿勢 $\bm{x} = f(\bm{\theta})$ を計算する写像でした。作業空間解析は、この写像 $f$ の値域を調べる問題に他なりません。

作業空間とは — 直感的理解

腕を伸ばしてみよう

自分の腕で試してみましょう。肩を固定して、腕をまっすぐ伸ばした状態でぐるりと回すと、指先は球面上を動きます。次に、肘を曲げた状態で回すと、指先が描く軌跡は先ほどより小さな球面になります。肘を最大限に曲げると、指先は肩の近くにしか届きません。

こうして「指先が到達できる全ての点の集合」を考えると、それは肩を中心とした球殻(中空の球)のような領域になります。外側の半径は腕を完全に伸ばしたときの長さ、内側の半径は腕を最大限に折り畳んだときに指先が肩に最も近づく距離です。

ロボットアームでも全く同じ考え方が成り立ちます。

数学的な定義

ロボットアームの作業空間(workspace)とは、エンドエフェクタが到達可能な位置(および姿勢)の集合です。$n$ 自由度のロボットアームの関節空間を $\mathcal{Q} \subseteq \mathbb{R}^n$ とし、順運動学の写像を $f: \mathcal{Q} \to \mathbb{R}^m$ とすると、作業空間 $\mathcal{W}$ は次のように定義されます。

$$ \begin{equation} \mathcal{W} = \{ \bm{x} \in \mathbb{R}^m \mid \bm{x} = f(\bm{\theta}), \; \bm{\theta} \in \mathcal{Q} \} \end{equation} $$

ここで $\mathcal{Q}$ は関節リミット(各関節の可動範囲 $\theta_{i,\min} \le \theta_i \le \theta_{i,\max}$)によって制約された関節空間であり、$m$ は作業空間の次元(2D平面なら $m = 2$、3D空間で位置のみなら $m = 3$、位置+姿勢なら $m = 6$)です。

式 (1) は、順運動学という「関数」に、許容される全ての関節角の組み合わせを「入力」したときに得られる「出力」の集合が作業空間である、と言っています。この集合がどんな形をしているかを調べるのが作業空間解析の本質です。

作業空間の形状は、リンクの長さ、関節の種類(回転関節・直動関節)、関節の可動範囲、そしてロボットのDHパラメータによって決まります。同じ自由度数でも、リンク長の比率が変われば作業空間の形状は大きく変わります。

では、作業空間は「届くか届かないか」の二値的な情報だけでしょうか?実は、届くにしても「どの方向からでもアプローチできる点」と「特定の姿勢でしか届かない点」があります。次節では、この違いを明確に分類しましょう。

到達可能作業空間と器用作業空間

2つの「届く」の違い

次の状況を考えてみてください。テーブルの中央にあるコップを取るとき、上から掴むこともできますし、横から掴むこともできます。手を回転させて好きな角度から掴めるでしょう。一方、テーブルの端にあるコップに腕をいっぱいに伸ばしてやっと届く場合、掴む方向は限られます。腕を完全に伸ばしているので手首をひねる自由がないのです。

ロボットアームでも同様に、届くこと自由に動けることは別の概念です。これを区別するために、2種類の作業空間が定義されます。

到達可能作業空間(Reachable Workspace)

到達可能作業空間 $\mathcal{W}_R$ は、エンドエフェクタが少なくとも1つの姿勢で到達できる位置の集合です。

$$ \begin{equation} \mathcal{W}_R = \{ \bm{p} \in \mathbb{R}^3 \mid \exists \, \bm{\theta} \in \mathcal{Q} \; \text{s.t.} \; \text{pos}(f(\bm{\theta})) = \bm{p} \} \end{equation} $$

ここで $\text{pos}(\cdot)$ は同次変換行列から位置成分を取り出す操作です。どんな姿勢でもよいので、とにかくその点に手先が届けば、その点は到達可能作業空間に含まれます。先ほどの例で言えば、テーブルの端のコップの位置は、腕を伸ばしきった特定の姿勢でしか届きませんが、それでも到達可能作業空間に含まれます。

器用作業空間(Dexterous Workspace)

器用作業空間 $\mathcal{W}_D$ は、エンドエフェクタが任意の姿勢で到達できる位置の集合です。

$$ \begin{equation} \mathcal{W}_D = \{ \bm{p} \in \mathbb{R}^3 \mid \forall \, \bm{R} \in SO(3), \; \exists \, \bm{\theta} \in \mathcal{Q} \; \text{s.t.} \; f(\bm{\theta}) = (\bm{p}, \bm{R}) \} \end{equation} $$

$SO(3)$ は3次元の回転群(全ての回転行列の集合)です。ある位置 $\bm{p}$ に対して、エンドエフェクタがどの方向を向いた状態でもその位置に到達できるなら、$\bm{p}$ は器用作業空間に属します。テーブルの中央のコップの位置は、上からでも横からでも掴めるので、器用作業空間に含まれる可能性が高いです。

包含関係

定義から明らかに、器用作業空間は到達可能作業空間の部分集合です。

$$ \begin{equation} \mathcal{W}_D \subseteq \mathcal{W}_R \end{equation} $$

「任意の姿勢で届く」は「少なくとも1つの姿勢で届く」よりも厳しい条件だからです。多くの実用的なロボットアームでは、器用作業空間は到達可能作業空間よりもかなり小さくなります。6自由度未満のロボットでは、器用作業空間が空集合(どの位置でも全方位からアプローチすることは不可能)になることもあります。

宇宙ロボティクスでの意味

ISS の Canadarm2 が太陽電池パネルを把持する場合、パネルの設置場所が到達可能作業空間に含まれるだけでは不十分です。パネルを正しい姿勢で取り付ける必要があるため、設置場所は理想的には器用作業空間内にあるべきです。少なくとも、必要な取り付け姿勢で到達できることを確認しなければなりません。

さらに、宇宙空間では重力がないため、地上のロボットとは異なる課題があります。自由浮遊ロボット(free-floating robot)の場合、ベースが固定されておらず、アームを動かすとベース自体が反動で動きます。これにより作業空間が変化する — これは地上ロボットにはない宇宙特有の問題です。本記事ではまず固定ベースのロボットを扱い、自由浮遊系の議論は今後の記事に譲ります。

到達可能作業空間と器用作業空間の概念を理解したところで、具体的にこれらの形状をどうやって求めるかを見ていきましょう。最も簡単な2リンクアームから始めて、解析的な方法を学びます。

解析的方法 — 2リンクの幾何学

なぜ2リンクから始めるのか

解析的に作業空間の形状を求められるのは、単純な構造のロボットに限られます。しかし、2リンクアームの解析は作業空間の本質的な性質を理解するのに最適です。2リンクで得られる直感は、より複雑なロボットにもそのまま拡張できます。

2リンク平面アーム

2次元平面内で動く2リンクアームを考えます。リンク長をそれぞれ $l_1$, $l_2$、関節角を $\theta_1$, $\theta_2$ とします。ベース(関節1)は原点に固定されています。

順運動学により、手先の位置 $(x, y)$ は次のように表されます。

$$ \begin{equation} x = l_1 \cos\theta_1 + l_2 \cos(\theta_1 + \theta_2) \end{equation} $$

$$ \begin{equation} y = l_1 \sin\theta_1 + l_2 \sin(\theta_1 + \theta_2) \end{equation} $$

手先の到達距離

原点から手先までの距離 $r$ を求めましょう。$r^2 = x^2 + y^2$ を計算します。

式 (5), (6) を代入すると

$$ r^2 = [l_1 \cos\theta_1 + l_2 \cos(\theta_1 + \theta_2)]^2 + [l_1 \sin\theta_1 + l_2 \sin(\theta_1 + \theta_2)]^2 $$

展開して整理します。$\cos^2\alpha + \sin^2\alpha = 1$ を使うと

$$ r^2 = l_1^2 + l_2^2 + 2l_1 l_2 [\cos\theta_1 \cos(\theta_1 + \theta_2) + \sin\theta_1 \sin(\theta_1 + \theta_2)] $$

角度の加法定理の逆 $\cos(A – B) = \cos A \cos B + \sin A \sin B$ を適用すると、角括弧内は $\cos\theta_2$ に簡約されます。

$$ \begin{equation} r^2 = l_1^2 + l_2^2 + 2l_1 l_2 \cos\theta_2 \end{equation} $$

この結果は非常に重要です。原点から手先までの距離 $r$ は第2関節角 $\theta_2$ のみで決まり、第1関節角 $\theta_1$ には依存しません。$\theta_1$ は手先の「方向」を変えるだけで「距離」には影響しないのです。

作業空間の形状:円環領域

式 (7) から、$r$ の最大値と最小値を求めます。

$\cos\theta_2$ が最大値 $1$ をとるとき($\theta_2 = 0$、つまり腕がまっすぐ伸びた状態)

$$ r_{\max} = l_1 + l_2 $$

$\cos\theta_2$ が最小値 $-1$ をとるとき($\theta_2 = \pi$、つまり腕が完全に折り畳まれた状態)

$$ r_{\min} = |l_1 – l_2| $$

$\theta_1$ が $0$ から $2\pi$ まで自由に回転できるとすると、距離 $r$ の手先は全方向に届きます。したがって、作業空間は原点を中心とする円環領域(annulus)になります。

$$ \begin{equation} \mathcal{W}_R = \{ (x, y) \in \mathbb{R}^2 \mid |l_1 – l_2| \le \sqrt{x^2 + y^2} \le l_1 + l_2 \} \end{equation} $$

特殊なケース

リンク長の比率によって作業空間の形状が変わります。

$l_1 = l_2$ の場合: $r_{\min} = 0$ となり、手先が原点に到達できます。作業空間は円環ではなく円板(disk)になります。人間の腕で言えば、上腕と前腕が同じ長さなら、肘を180度折り畳むことで指先を肩まで持ってこられることに対応します。

$l_1 \gg l_2$ の場合: $r_{\min} \approx l_1$ となり、作業空間は細い円環になります。長い棒の先に短い手がついているような構造で、到達範囲は広いものの、内側の領域には届きません。

$l_1 \ll l_2$ の場合: 同様に $r_{\min} \approx l_2$ で、こちらも細い円環です。ただし全体的に大きな円になります。

3リンク以上での困難さ

3自由度以上のロボットでは、作業空間の境界を解析的に求めることが急激に難しくなります。3リンク平面アームでも、作業空間の内部構造(到達可能な点での自由度の冗長性など)が複雑になります。6自由度の産業用ロボットや、7自由度の冗長マニピュレータでは、解析的アプローチは実用的ではありません。

そこで、コンピュータの力を借りた数値的方法が必要になります。最もシンプルで強力な方法がモンテカルロサンプリングです。

数値的方法 — モンテカルロサンプリング

アイデア

モンテカルロ法による作業空間推定のアイデアは驚くほどシンプルです。

  1. 関節空間 $\mathcal{Q}$ からランダムに関節角度ベクトル $\bm{\theta}$ をサンプリングする
  2. 順運動学 $f(\bm{\theta})$ で手先位置を計算する
  3. これを大量に繰り返す
  4. 得られた手先位置の集合をプロットする

十分な数のサンプルを取れば、プロットされた点が作業空間を「塗りつぶす」ことになります。点が密集している領域は到達しやすい場所、まばらな領域は到達しにくい場所(特定の関節角の組み合わせでしか到達できない場所)を意味します。

アルゴリズム

モンテカルロサンプリングによる作業空間推定のアルゴリズムを整理します。

入力: DH パラメータ、関節リミット $[\theta_{i,\min}, \theta_{i,\max}]$、サンプル数 $N$

手順:

  1. $k = 1, 2, \dots, N$ について: – 各関節 $i$ について $\theta_i^{(k)} \sim \text{Uniform}(\theta_{i,\min}, \theta_{i,\max})$ をサンプリング – 順運動学を計算: $\bm{p}^{(k)} = \text{pos}(f(\bm{\theta}^{(k)}))$ – $\bm{p}^{(k)}$ を記録
  2. $\{\bm{p}^{(1)}, \bm{p}^{(2)}, \dots, \bm{p}^{(N)}\}$ をプロット

出力: 作業空間の近似的な点群表現

長所と短所

長所:

  • 実装が極めて簡単: 順運動学さえ計算できれば、任意のロボットに適用できる
  • 自由度数に依存しない: 2自由度でも20自由度でも同じアルゴリズム
  • 確率的な密度情報が得られる: 点の密度から「到達しやすさ」の情報が自然に得られる

短所:

  • 境界の精度が低い: 作業空間の正確な境界を知るには大量のサンプルが必要
  • 高次元空間での効率: 自由度が増えると、関節空間の体積が指数的に増大するため、同じ精度を得るにはサンプル数を大幅に増やす必要がある(次元の呪い)
  • 器用作業空間の推定が困難: ランダムサンプリングでは「全ての姿勢で到達可能か」を判定するのが難しい

点密度と到達容易性

モンテカルロ法で得られる点の密度は、単なる描画上のアーティファクトではなく、物理的な意味を持ちます。関節空間で一様にサンプリングしたとき、作業空間のある点 $\bm{p}$ 近傍に落ちるサンプルの数は、$\bm{p}$ に到達する逆運動学解の数(およびそれらの近傍の「体積」)に比例します。

つまり、サンプル密度が高い領域は多くの関節角の組み合わせで到達できる「冗長性の高い」領域であり、逆にサンプル密度が低い領域は限られた姿勢でしか到達できない「ギリギリ」の領域です。ロボットの作業計画では、密度の高い領域で作業するのが好ましい — 関節角の自由度に余裕があり、障害物回避や姿勢の最適化が可能だからです。

この密度情報は解析的方法では得られない、モンテカルロ法ならではの利点です。ここまでの議論では関節角が自由に動けることを仮定してきましたが、現実のロボットでは関節の可動範囲に制限があります。次にこの制約の影響を見ていきましょう。

関節リミットの影響

現実のロボットには可動範囲がある

理想的な回転関節は $0$ から $2\pi$(360度)まで自由に回転できますが、現実のロボットアームの関節はそうはいきません。ケーブルの巻き付き防止、機械的な干渉の回避、安全上の制約などにより、各関節には可動範囲(関節リミット)が設定されています。

例えば、典型的な産業用6軸ロボットの関節リミットは次のようなものです。

関節 可動範囲 備考
$\theta_1$(ベース旋回) $-170° \sim +170°$ 背面にデッドゾーン
$\theta_2$(肩) $-90° \sim +90°$ 上下方向の制限
$\theta_3$(肘) $-170° \sim +170°$ 折り畳みの制限
$\theta_4$(手首旋回) $-180° \sim +180°$ ケーブル保護
$\theta_5$(手首曲げ) $-120° \sim +120°$ 特異姿勢回避
$\theta_6$(ツール回転) $-360° \sim +360°$ ツール接続部の制約

数学的な表現

関節リミットは、関節空間 $\mathcal{Q}$ を超直方体(hyperrectangle)に制約します。

$$ \begin{equation} \mathcal{Q} = \{ \bm{\theta} \in \mathbb{R}^n \mid \theta_{i,\min} \le \theta_i \le \theta_{i,\max}, \; i = 1, 2, \dots, n \} \end{equation} $$

関節リミットなし(全関節が $[0, 2\pi]$ で自由に回転可能)の場合の作業空間を $\mathcal{W}_{\text{full}}$、関節リミットありの場合を $\mathcal{W}_{\text{limited}}$ とすると、明らかに

$$ \begin{equation} \mathcal{W}_{\text{limited}} \subseteq \mathcal{W}_{\text{full}} \end{equation} $$

が成り立ちます。関節リミットは作業空間を「削る」方向にしか作用しません。

2リンクアームでの具体例

先ほどの2リンクアームで、関節リミットの影響を具体的に見てみましょう。

ケース1: $\theta_1 \in [0, 2\pi]$, $\theta_2 \in [0, 2\pi]$(制限なし)

→ 作業空間は完全な円環

ケース2: $\theta_1 \in [0, \pi]$, $\theta_2 \in [0, 2\pi]$(第1関節が半回転のみ)

→ 円環の上半分のみ

ケース3: $\theta_1 \in [0, 2\pi]$, $\theta_2 \in [-\pi/2, \pi/2]$(第2関節の曲がりが制限)

→ 円環だが内径が大きくなる(完全に折り畳めないため)

このように、どの関節にリミットがあるかによって作業空間の形が大きく変わります。第1関節(ベース旋回)のリミットは作業空間の「角度範囲」を制限し、肘や手首の関節のリミットは「到達距離の範囲」を制限する傾向があります。

宇宙ロボットと関節リミット

宇宙ロボットでは、関節リミットの設計が特に重要です。ISS の Canadarm2 は、関節リミットを広く取る代わりに7自由度(冗長自由度)を持つ設計になっています。冗長な1自由度を使うことで、関節リミットを避けながら同じ手先位置に到達する複数の姿勢を選べます。これにより、関節リミットによる作業空間の縮小を最小限に抑えています。

また、宇宙空間では保守(メンテナンス)が困難なため、関節が可動範囲の限界付近で動作し続けると機械的な摩耗が加速するリスクがあります。そのため、実際の運用では関節リミットよりもさらに内側の「ソフトリミット」を設定し、余裕を持った動作を心がけることが一般的です。

関節リミットが作業空間を縮小させることを見ましたが、実際の作業環境ではさらに厄介な制約があります。それが障害物の存在です。

障害物を考慮した有効作業空間

障害物による制約

現実の作業環境では、ロボットの周囲に障害物(壁、柱、他の機器、人間など)が存在します。ロボットアームはこれらの障害物と衝突してはならないため、たとえ運動学的に到達可能な点であっても、障害物と干渉する姿勢でしかたどり着けない点は実質的に到達不可能です。

障害物を考慮した作業空間を有効作業空間(effective workspace)または自由作業空間(free workspace)と呼びます。

数学的な定式化

障害物の集合を $\mathcal{O} \subset \mathbb{R}^3$ とし、関節角 $\bm{\theta}$ のときにロボットアームが占める空間を $\mathcal{A}(\bm{\theta}) \subset \mathbb{R}^3$ とすると、衝突のない関節角の集合は

$$ \begin{equation} \mathcal{Q}_{\text{free}} = \{ \bm{\theta} \in \mathcal{Q} \mid \mathcal{A}(\bm{\theta}) \cap \mathcal{O} = \emptyset \} \end{equation} $$

であり、有効作業空間は

$$ \begin{equation} \mathcal{W}_{\text{free}} = \{ \bm{p} \in \mathbb{R}^3 \mid \exists \, \bm{\theta} \in \mathcal{Q}_{\text{free}} \; \text{s.t.} \; \text{pos}(f(\bm{\theta})) = \bm{p} \} \end{equation} $$

と定義されます。

ここで注意すべきは、障害物との干渉チェックが手先だけでなくロボットの全リンクについて必要であることです。手先が障害物に触れていなくても、肘が壁にぶつかることは十分にあり得ます。

干渉チェックの方法

障害物との干渉チェックにはいくつかの方法があります。

幾何学的方法: ロボットの各リンクを円柱や直方体で近似し、障害物(球、直方体などの基本形状)との交差判定を解析的に行います。計算は高速ですが、複雑な形状には対応しにくい欠点があります。

サンプリングベース法: ロボットの表面上の代表点(複数の点でリンクを近似)が障害物内にあるかを判定します。精度はサンプル点の数に依存しますが、任意の形状に対応できます。

ボクセル法: 空間をボクセル(3次元グリッド)に分割し、ロボットと障害物がそれぞれどのボクセルを占有するかを事前計算します。実行時はボクセルの重なりを判定するだけなので高速です。

宇宙での障害物

ISS の船外活動では、以下のようなものが障害物となります。

  • ISS の構造体: トラス構造、モジュール、ハンドレール
  • 太陽電池パネル: 大きな平面構造で、回転してアームの可動域に入ることがある
  • 放熱パネル(ラジエーター): 熱制御のための大型パネル
  • 宇宙飛行士: 船外活動中の飛行士がいる場合、衝突は絶対に避けなければならない
  • ケーブルやハーネス: ISS 外壁を這うケーブル類

これらの障害物は位置が変化する場合もあるため(太陽電池パネルの回転、宇宙飛行士の移動)、有効作業空間はリアルタイムで再計算が必要になることもあります。

ここまでの議論で、作業空間の「境界」を知る方法を学びました。しかし、実際のタスクプランニングでは「届くか否か」だけでなく「どの程度自由に動けるか」という定量的な指標が欲しくなります。これを提供するのがリーチャビリティマップです。

リーチャビリティマップ

到達能力の定量化

到達可能作業空間は「届くか届かないか」の二値情報しか持ちませんが、実際の作業計画では届く「質」が重要です。ある位置に対して、エンドエフェクタが何通りの方向からアプローチできるかは、作業の柔軟性に直結します。

リーチャビリティマップ(reachability map)は、作業空間の各点に「到達能力の指標」(リーチャビリティインデックス)を割り当てた空間的なマップです。

リーチャビリティインデックスの定義

作業空間を3次元グリッドに離散化し、各グリッドセル $c$ に対してリーチャビリティインデックス $\mathcal{R}(c)$ を次のように定義します。

まず、$N_{\text{dir}}$ 個の到達方向(エンドエフェクタの姿勢)を球面上に一様に分布させます。例えば $N_{\text{dir}} = 200$ 個の方向を用意します。

セル $c$ の中心位置 $\bm{p}_c$ に対して、各方向 $\bm{d}_k$($k = 1, 2, \dots, N_{\text{dir}}$)からの到達を試みます。逆運動学が解を持てば、その方向は「到達可能」です。到達可能な方向の数を $n_{\text{reach}}(c)$ とすると

$$ \begin{equation} \mathcal{R}(c) = \frac{n_{\text{reach}}(c)}{N_{\text{dir}}} \end{equation} $$

$\mathcal{R}(c) = 1$ は全方向から到達可能(完全な器用性)、$\mathcal{R}(c) = 0$ はどの方向からも到達不可能(作業空間外)を意味します。

リーチャビリティマップの構築アルゴリズム

  1. 作業空間を包含する3次元グリッドを定義
  2. 球面上に $N_{\text{dir}}$ 個の方向を一様にサンプリング(Fibonacci格子法など)
  3. 各グリッドセル $c$ について: – 各方向 $\bm{d}_k$ について逆運動学を解く – 解が存在するかどうかを判定 – $\mathcal{R}(c)$ を計算
  4. 結果を可視化(カラーマップ、等値面など)

可操作性との関係

リーチャビリティマップに類似した概念として、可操作性(manipulability)があります。吉川の可操作性楕円体(manipulability ellipsoid)は、ある関節角 $\bm{\theta}$ でのヤコビ行列 $\bm{J}(\bm{\theta})$ を用いて

$$ \begin{equation} w(\bm{\theta}) = \sqrt{\det(\bm{J}(\bm{\theta}) \bm{J}(\bm{\theta})^T)} \end{equation} $$

と定義されます。$w(\bm{\theta}) = 0$ は特異姿勢(ある方向に手先が動けない状態)を意味し、$w$ が大きいほど手先が全方向に均等に動けることを示します。

リーチャビリティマップが「その位置に何通りの方向から到達できるか」を示すのに対し、可操作性は「現在の姿勢でどれだけ自由に動けるか」を示します。両者は相補的な情報を提供し、組み合わせることでより包括的なロボットの能力評価が可能になります。

宇宙ロボティクスでの活用

リーチャビリティマップは宇宙ロボットの設計と運用の両方で活用されます。

設計段階: ロボットアームのリンク長や関節配置を変えたときにリーチャビリティマップがどう変化するかをシミュレーションし、ミッション要求(例えば「ISS の全モジュールの外壁に 80% 以上のリーチャビリティで到達できること」)を満たす設計を探索します。

運用段階: ロボットのベース位置(Canadarm2 は ISS 外壁のレール上を移動可能)をどこにすれば目標タスクに最適かを、リーチャビリティマップを参照して決定します。

故障時の対応: ある関節が故障して固定された場合、リーチャビリティマップを再計算することで、残りの自由度でどの程度の作業が可能かを迅速に評価できます。これは宇宙空間での修理が困難な状況で特に重要です。

ここまでの理論的な議論を踏まえて、いよいよ Python で実際に作業空間を解析し、可視化してみましょう。

Pythonでの実装と可視化

2リンク平面アームの作業空間(解析解との比較)

まず、最も基本的な2リンク平面アームでモンテカルロサンプリングを行い、解析解(円環領域)と比較します。理論で導いた結果をコードで確認することで、手法の妥当性を検証します。

import numpy as np
import matplotlib.pyplot as plt

# --- 2リンク平面アームの順運動学 ---
def forward_kinematics_2link(theta1, theta2, l1, l2):
    """2リンクアームの手先位置を計算"""
    x = l1 * np.cos(theta1) + l2 * np.cos(theta1 + theta2)
    y = l1 * np.sin(theta1) + l2 * np.sin(theta1 + theta2)
    return x, y

# パラメータ
l1, l2 = 1.0, 0.7  # リンク長
N = 50000           # サンプル数

# モンテカルロサンプリング(関節リミットなし)
theta1 = np.random.uniform(0, 2 * np.pi, N)
theta2 = np.random.uniform(0, 2 * np.pi, N)
x, y = forward_kinematics_2link(theta1, theta2, l1, l2)

# 解析解: 円環の内径と外径
r_min = abs(l1 - l2)  # |1.0 - 0.7| = 0.3
r_max = l1 + l2        # 1.0 + 0.7 = 1.7

# 可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 左: モンテカルロサンプリング結果
axes[0].scatter(x, y, s=0.1, alpha=0.3, c='cyan')
circle_outer = plt.Circle((0, 0), r_max, fill=False, color='red',
                           linestyle='--', linewidth=2, label=f'$r_{{max}}={r_max}$')
circle_inner = plt.Circle((0, 0), r_min, fill=False, color='orange',
                           linestyle='--', linewidth=2, label=f'$r_{{min}}={r_min}$')
axes[0].add_patch(circle_outer)
axes[0].add_patch(circle_inner)
axes[0].set_xlim(-2, 2)
axes[0].set_ylim(-2, 2)
axes[0].set_aspect('equal')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('Monte Carlo Sampling (No Joint Limits)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 右: 点密度ヒストグラム(径方向)
r = np.sqrt(x**2 + y**2)
axes[1].hist(r, bins=100, density=True, color='steelblue', alpha=0.7,
             edgecolor='black', linewidth=0.3)
axes[1].axvline(r_min, color='orange', linestyle='--', linewidth=2,
                label=f'$r_{{min}}={r_min}$')
axes[1].axvline(r_max, color='red', linestyle='--', linewidth=2,
                label=f'$r_{{max}}={r_max}$')
axes[1].set_xlabel('Distance from origin $r$')
axes[1].set_ylabel('Probability density')
axes[1].set_title('Radial Distribution of End-Effector')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('workspace_2link_basic.png', dpi=150, bbox_inches='tight')
plt.show()

左のプロットでは、モンテカルロサンプリングの結果(水色の点群)が解析解の円環領域(赤とオレンジの破線で示した外径・内径)にぴったり収まっていることが確認できます。50,000 点のサンプルで作業空間がきれいに塗りつぶされており、理論的に予測した円環形状が正しいことが実証されました。

右のヒストグラムは、手先の原点からの距離 $r$ の分布を示しています。$r_{\min} = 0.3$ と $r_{\max} = 1.7$ の範囲外にはサンプルが存在しないことが確認できます。また、分布のピークが $r \approx 1.2$ 付近($r_{\min}$ と $r_{\max}$ の中間よりやや大きい位置)にあることに注目してください。これは、肘を適度に曲げた姿勢が最も多くの関節角の組み合わせで実現できることを反映しています。完全に伸ばした姿勢($r = r_{\max}$)や完全に折り畳んだ姿勢($r = r_{\min}$)は、$\theta_2$ が特定の値に限られるため、確率的に稀なのです。

関節リミットの影響を可視化

次に、関節リミットが作業空間をどのように変形させるかを見てみましょう。3種類の異なる関節リミット条件を比較します。

import numpy as np
import matplotlib.pyplot as plt

def forward_kinematics_2link(theta1, theta2, l1, l2):
    """2リンクアームの手先位置を計算"""
    x = l1 * np.cos(theta1) + l2 * np.cos(theta1 + theta2)
    y = l1 * np.sin(theta1) + l2 * np.sin(theta1 + theta2)
    return x, y

l1, l2 = 1.0, 0.7
N = 50000

# 3つの関節リミット条件
conditions = [
    {'label': 'No Limits\n$\\theta_1 \\in [0, 2\\pi]$, $\\theta_2 \\in [0, 2\\pi]$',
     'theta1_range': (0, 2*np.pi), 'theta2_range': (0, 2*np.pi)},
    {'label': 'Base Limited\n$\\theta_1 \\in [0, \\pi]$, $\\theta_2 \\in [0, 2\\pi]$',
     'theta1_range': (0, np.pi), 'theta2_range': (0, 2*np.pi)},
    {'label': 'Elbow Limited\n$\\theta_1 \\in [0, 2\\pi]$, $\\theta_2 \\in [-\\pi/3, \\pi/3]$',
     'theta1_range': (0, 2*np.pi), 'theta2_range': (-np.pi/3, np.pi/3)},
]

fig, axes = plt.subplots(1, 3, figsize=(18, 6))

for ax, cond in zip(axes, conditions):
    t1 = np.random.uniform(*cond['theta1_range'], N)
    t2 = np.random.uniform(*cond['theta2_range'], N)
    x, y = forward_kinematics_2link(t1, t2, l1, l2)

    ax.scatter(x, y, s=0.1, alpha=0.3, c='cyan')
    ax.set_xlim(-2, 2)
    ax.set_ylim(-2, 2)
    ax.set_aspect('equal')
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title(cond['label'])
    ax.grid(True, alpha=0.3)
    # 原点(ベース)をマーク
    ax.plot(0, 0, 'ro', markersize=8, label='Base')
    ax.legend()

plt.tight_layout()
plt.savefig('workspace_joint_limits.png', dpi=150, bbox_inches='tight')
plt.show()

3つのプロットを比較すると、関節リミットの影響が視覚的に明確にわかります。

左図(制限なし)は完全な円環で、これが先ほどの理論解と一致する基準ケースです。中央図(ベース関節を半回転に制限)では、作業空間が上半分だけになっています。第1関節 $\theta_1 \in [0, \pi]$ は手先の「方向」を制御するため、作業空間が角度方向に切り取られるのです。右図(肘関節を $\pm 60°$ に制限)では、作業空間は全方位にわたりますが、内径が大きくなり細い帯状になっています。肘があまり曲がらないため、手先は原点の近くに到達できず、遠い位置にも制限があります。

このように、どの関節にリミットがかかるかによって作業空間への影響パターンが質的に異なることが確認できました。

3リンク平面アームの作業空間

2リンクでは円環という単純な形状でしたが、3リンクになると作業空間はどう変わるでしょうか。3リンクアームは3自由度あるため、2次元平面内では「冗長」(必要な2自由度より多い)です。

import numpy as np
import matplotlib.pyplot as plt

def forward_kinematics_3link(theta1, theta2, theta3, l1, l2, l3):
    """3リンクアームの手先位置を計算"""
    x = (l1 * np.cos(theta1)
         + l2 * np.cos(theta1 + theta2)
         + l3 * np.cos(theta1 + theta2 + theta3))
    y = (l1 * np.sin(theta1)
         + l2 * np.sin(theta1 + theta2)
         + l3 * np.sin(theta1 + theta2 + theta3))
    return x, y

# パラメータ
l1, l2, l3 = 1.0, 0.7, 0.4
N = 100000

# 制限なしの場合
theta1 = np.random.uniform(0, 2*np.pi, N)
theta2 = np.random.uniform(0, 2*np.pi, N)
theta3 = np.random.uniform(0, 2*np.pi, N)
x_full, y_full = forward_kinematics_3link(theta1, theta2, theta3,
                                           l1, l2, l3)

# 関節リミットありの場合
theta1_lim = np.random.uniform(-np.pi*2/3, np.pi*2/3, N)
theta2_lim = np.random.uniform(-np.pi*3/4, np.pi*3/4, N)
theta3_lim = np.random.uniform(-np.pi/2, np.pi/2, N)
x_lim, y_lim = forward_kinematics_3link(theta1_lim, theta2_lim, theta3_lim,
                                          l1, l2, l3)

# 解析的な限界
r_max_3 = l1 + l2 + l3  # 2.1
r_min_3 = max(0, l1 - l2 - l3)  # 0 (l1 < l2+l3 なので)

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 左: 制限なし
axes[0].scatter(x_full, y_full, s=0.1, alpha=0.2, c='cyan')
circle = plt.Circle((0, 0), r_max_3, fill=False, color='red',
                     linestyle='--', linewidth=2, label=f'$r_{{max}}={r_max_3}$')
axes[0].add_patch(circle)
axes[0].set_xlim(-2.5, 2.5)
axes[0].set_ylim(-2.5, 2.5)
axes[0].set_aspect('equal')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('3-Link Arm (No Limits)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 右: 関節リミットあり
axes[1].scatter(x_lim, y_lim, s=0.1, alpha=0.2, c='lime')
axes[1].set_xlim(-2.5, 2.5)
axes[1].set_ylim(-2.5, 2.5)
axes[1].set_aspect('equal')
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title('3-Link Arm (With Joint Limits)')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('workspace_3link.png', dpi=150, bbox_inches='tight')
plt.show()

左図の3リンクアーム(制限なし)では、作業空間が完全な円板($r_{\min} = 0$)になっていることがわかります。$l_1 = 1.0 < l_2 + l_3 = 1.1$ であるため、第2・第3リンクを適切に折り畳むことで原点に到達でき、内側の穴がなくなるのです。また、点の密度が中心付近で最も高く、外側に向かって薄くなっていることにも注目してください。これは3つの関節角の自由度があるため、近い位置ほど多くの組み合わせで到達できることを反映しています。

右図(関節リミットあり)では、作業空間が非対称な形状に変化しています。第1関節が $\pm 120°$ に制限されているため、背面側($x < 0$ の遠方)には到達できなくなっています。形状は単純な幾何学図形では表現できず、数値的手法の必要性が実感できます。

障害物を考慮した有効作業空間

次に、障害物が存在する場合の有効作業空間を可視化します。2リンクアームの周囲に円形障害物を配置し、リンクが障害物と干渉する姿勢を除外します。

import numpy as np
import matplotlib.pyplot as plt

def forward_kinematics_2link_full(theta1, theta2, l1, l2):
    """2リンクアームの各関節位置を計算"""
    # 関節1(肘)の位置
    x1 = l1 * np.cos(theta1)
    y1 = l1 * np.sin(theta1)
    # 手先の位置
    x2 = x1 + l2 * np.cos(theta1 + theta2)
    y2 = y1 + l2 * np.sin(theta1 + theta2)
    return x1, y1, x2, y2

def check_link_collision(x_start, y_start, x_end, y_end,
                         obs_x, obs_y, obs_r, n_check=10):
    """リンク(線分)と円形障害物の干渉チェック"""
    # リンク上の n_check 個の点をサンプリング
    t_vals = np.linspace(0, 1, n_check)
    for t in t_vals:
        px = x_start + t * (x_end - x_start)
        py = y_start + t * (y_end - y_start)
        dist = np.sqrt((px - obs_x)**2 + (py - obs_y)**2)
        if dist < obs_r:
            return True  # 衝突
    return False  # 衝突なし

# パラメータ
l1, l2 = 1.0, 0.7
N = 30000

# 障害物の定義(円形)
obstacles = [
    {'x': 0.8, 'y': 0.8, 'r': 0.3},
    {'x': -0.5, 'y': 1.0, 'r': 0.25},
]

# モンテカルロサンプリング
theta1 = np.random.uniform(0, 2*np.pi, N)
theta2 = np.random.uniform(0, 2*np.pi, N)

x_free, y_free = [], []
x_collision, y_collision = [], []

for i in range(N):
    x1, y1, x2, y2 = forward_kinematics_2link_full(
        theta1[i], theta2[i], l1, l2)

    collision = False
    for obs in obstacles:
        # リンク1(原点 → 肘)のチェック
        if check_link_collision(0, 0, x1, y1,
                                obs['x'], obs['y'], obs['r']):
            collision = True
            break
        # リンク2(肘 → 手先)のチェック
        if check_link_collision(x1, y1, x2, y2,
                                obs['x'], obs['y'], obs['r']):
            collision = True
            break

    if collision:
        x_collision.append(x2)
        y_collision.append(y2)
    else:
        x_free.append(x2)
        y_free.append(y2)

# 可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 左: 障害物なしの作業空間
x_all, y_all = forward_kinematics_2link_full(theta1, theta2, l1, l2)[2:]
axes[0].scatter(x_all, y_all, s=0.3, alpha=0.3, c='cyan', label='Workspace')
for obs in obstacles:
    circle = plt.Circle((obs['x'], obs['y']), obs['r'],
                         color='red', alpha=0.5)
    axes[0].add_patch(circle)
axes[0].set_xlim(-2, 2)
axes[0].set_ylim(-2, 2)
axes[0].set_aspect('equal')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('Full Workspace (Obstacles Shown)')
axes[0].grid(True, alpha=0.3)

# 右: 障害物考慮の有効作業空間
axes[1].scatter(x_free, y_free, s=0.3, alpha=0.3, c='lime',
                label='Free workspace')
for obs in obstacles:
    circle = plt.Circle((obs['x'], obs['y']), obs['r'],
                         color='red', alpha=0.5, label='Obstacle')
    axes[1].add_patch(circle)
axes[1].set_xlim(-2, 2)
axes[1].set_ylim(-2, 2)
axes[1].set_aspect('equal')
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title('Effective (Free) Workspace')
axes[1].legend(loc='upper right')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('workspace_obstacles.png', dpi=150, bbox_inches='tight')
plt.show()

左図は障害物を配置した状態での全作業空間、右図は障害物との干渉を除外した有効作業空間です。障害物(赤い円)の近傍では、到達可能な点が明らかに「刈り取られて」いることが確認できます。

特に重要なのは、障害物の直接的な位置だけでなく、その周囲の広い領域が影響を受けていることです。これは、障害物の近くの点に到達する際に第1リンク(肘)が障害物を通過する姿勢が除外されるためです。手先の到達位置が障害物から離れていても、途中のリンクが障害物と干渉するケースは多く、有効作業空間は直感よりも大幅に狭くなることがあります。

2Dリーチャビリティマップ

ここでは、2リンクアームの各位置に「到達可能な姿勢の数」を色で表したリーチャビリティマップを構築します。これにより、到達可能性の「濃淡」が可視化されます。

import numpy as np
import matplotlib.pyplot as plt

def forward_kinematics_2link(theta1, theta2, l1, l2):
    """2リンクアームの手先位置を計算"""
    x = l1 * np.cos(theta1) + l2 * np.cos(theta1 + theta2)
    y = l1 * np.sin(theta1) + l2 * np.sin(theta1 + theta2)
    return x, y

# パラメータ
l1, l2 = 1.0, 0.7
N = 200000  # 多めにサンプリング

# モンテカルロサンプリング
theta1 = np.random.uniform(0, 2*np.pi, N)
theta2 = np.random.uniform(0, 2*np.pi, N)
x, y = forward_kinematics_2link(theta1, theta2, l1, l2)

# 手先の方向角(リーチャビリティの代替指標)
phi = theta1 + theta2  # 手先のリンク方向
phi = np.mod(phi, 2*np.pi)

# 2Dグリッドでリーチャビリティを計算
grid_res = 0.05
x_bins = np.arange(-2, 2 + grid_res, grid_res)
y_bins = np.arange(-2, 2 + grid_res, grid_res)
n_dir_bins = 8  # 方向を8分割

# 各グリッドセルで到達可能な方向数をカウント
reach_map = np.zeros((len(y_bins)-1, len(x_bins)-1))

# グリッドインデックスの計算
ix = np.digitize(x, x_bins) - 1
iy = np.digitize(y, y_bins) - 1
iphi = np.digitize(phi, np.linspace(0, 2*np.pi, n_dir_bins+1)) - 1

# 有効なインデックスのみ処理
valid = ((ix >= 0) & (ix < len(x_bins)-1) &
         (iy >= 0) & (iy < len(y_bins)-1))

# 各セルの方向カバレッジを計算
direction_sets = {}
for i in range(N):
    if not valid[i]:
        continue
    key = (iy[i], ix[i])
    if key not in direction_sets:
        direction_sets[key] = set()
    direction_sets[key].add(iphi[i])

for key, dirs in direction_sets.items():
    reach_map[key[0], key[1]] = len(dirs) / n_dir_bins

# 作業空間外をマスク
reach_map_masked = np.ma.masked_where(reach_map == 0, reach_map)

# 可視化
fig, ax = plt.subplots(figsize=(8, 8))
im = ax.pcolormesh(x_bins, y_bins, reach_map_masked,
                    cmap='plasma', vmin=0, vmax=1)
cbar = plt.colorbar(im, ax=ax, label='Reachability Index')
ax.set_aspect('equal')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('2D Reachability Map (2-Link Arm)')

# 円環の境界を追加
theta_circle = np.linspace(0, 2*np.pi, 200)
r_max = l1 + l2
r_min = abs(l1 - l2)
ax.plot(r_max*np.cos(theta_circle), r_max*np.sin(theta_circle),
        'w--', linewidth=1.5, alpha=0.7, label=f'$r_{{max}}={r_max}$')
ax.plot(r_min*np.cos(theta_circle), r_min*np.sin(theta_circle),
        'w--', linewidth=1.5, alpha=0.7, label=f'$r_{{min}}={r_min}$')
ax.legend(loc='upper right')

plt.tight_layout()
plt.savefig('reachability_map_2d.png', dpi=150, bbox_inches='tight')
plt.show()

リーチャビリティマップでは、色が明るい(リーチャビリティインデックスが高い)領域ほど、多くの方向からエンドエフェクタが到達できることを意味します。

マップから以下の重要な知見が得られます。

  1. 内径付近と外径付近はリーチャビリティが低い(暗い色): 腕を伸ばしきった姿勢(外径付近)や完全に折り畳んだ姿勢(内径付近)では、手先の方向の自由度がほとんどなく、限られた方向からしかアプローチできません。
  2. 中間の距離でリーチャビリティが最大(明るい色): 適度に肘を曲げた姿勢が最も多くの方向自由度を持つことがわかります。これは直感にも合います — 腕に余裕があるときが最も器用に動けるのです。
  3. リーチャビリティの等高線は同心円状: 2リンクアームの対称性から当然ですが、到達能力は原点からの距離のみに依存します。関節リミットがあるとこの対称性は崩れます。

この結果は、ロボットの設計やタスクプランニングに直接的な示唆を与えます。重要な作業(精密な組み立てなど)は、リーチャビリティが高い領域で行うべきです。

3Dロボットアームの作業空間

最後に、3次元空間で動くロボットアームの作業空間を可視化します。ここでは、3自由度(3回転関節)の空間アームを取り上げます。

import numpy as np
import matplotlib.pyplot as plt

def forward_kinematics_3dof_spatial(theta1, theta2, theta3,
                                     l1, l2, l3):
    """
    3自由度空間アームの順運動学
    関節1: z軸まわり回転(ベース旋回)
    関節2: y'軸まわり回転(肩)
    関節3: y''軸まわり回転(肘)
    """
    # 関節2,3による平面内の到達距離と高さ
    r_planar = (l2 * np.cos(theta2)
                + l3 * np.cos(theta2 + theta3))
    z = (l1
         + l2 * np.sin(theta2)
         + l3 * np.sin(theta2 + theta3))

    # 関節1によるベース旋回
    x = r_planar * np.cos(theta1)
    y = r_planar * np.sin(theta1)

    return x, y, z

# パラメータ
l1 = 0.3   # ベース高さ
l2 = 1.0   # 上腕
l3 = 0.7   # 前腕
N = 100000

# モンテカルロサンプリング
theta1 = np.random.uniform(0, 2*np.pi, N)
theta2 = np.random.uniform(-np.pi/2, np.pi, N)
theta3 = np.random.uniform(-np.pi, np.pi, N)

x, y, z = forward_kinematics_3dof_spatial(theta1, theta2, theta3,
                                            l1, l2, l3)

# 3Dプロット
fig = plt.figure(figsize=(14, 6))

# 左: 3D散布図
ax1 = fig.add_subplot(121, projection='3d')
# 到達距離で色付け
r = np.sqrt(x**2 + y**2 + (z - l1)**2)
subsample = np.random.choice(N, min(20000, N), replace=False)
sc = ax1.scatter(x[subsample], y[subsample], z[subsample],
                  c=r[subsample], cmap='plasma', s=0.3, alpha=0.3)
ax1.set_xlabel('X')
ax1.set_ylabel('Y')
ax1.set_zlabel('Z')
ax1.set_title('3-DOF Spatial Arm Workspace')
plt.colorbar(sc, ax=ax1, label='Distance from shoulder',
             shrink=0.6, pad=0.1)

# 右: XZ断面(y≈0のスライス)
ax2 = fig.add_subplot(122)
mask_xz = np.abs(y) < 0.1
ax2.scatter(x[mask_xz], z[mask_xz], s=0.5, alpha=0.3,
            c='cyan')
ax2.set_xlabel('X (or radial distance)')
ax2.set_ylabel('Z (height)')
ax2.set_title('XZ Cross-Section (|y| < 0.1)')
ax2.set_aspect('equal')
ax2.grid(True, alpha=0.3)
ax2.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)
ax2.axvline(x=0, color='gray', linestyle='-', linewidth=0.5)
# ベース位置
ax2.plot(0, l1, 'ro', markersize=8, label='Shoulder')
ax2.legend()

plt.tight_layout()
plt.savefig('workspace_3d.png', dpi=150, bbox_inches='tight')
plt.show()

左の3D散布図では、ロボットアームの作業空間がドーナツ(トーラス)のような形状を持つことがわかります。ベース旋回($\theta_1$)によって軸対称であり、断面形状は2リンクアームの円環に対応します。色は肩関節からの距離を表しており、外側(明るい色)は腕を伸ばした状態、内側(暗い色)は折り畳んだ状態に対応します。

右のXZ断面図は $y \approx 0$ の平面でスライスしたもので、2リンクアームで見た円環形状がそのまま現れています。ただし、重力の影響はないと仮定しているため(宇宙環境を想定)、上下方向に対称な形状になっています。地上のロボットでは重力の影響で下方への作業がしやすくなるなど、非対称性が生じます。

作業空間のZ方向の広がりに注目すると、ベースの高さ $l_1 = 0.3$ を中心として、肩を上げた姿勢($z$ が大きい)と下げた姿勢($z$ が小さい)の両方に到達していることがわかります。宇宙ロボットの場合、この全方向への到達能力が重要です — 微小重力環境では「上下」の概念がなく、あらゆる方向にある作業対象にアクセスする必要があるためです。

リーチャビリティの径方向プロファイル

最後に、3D作業空間のリーチャビリティを径方向の距離に対してプロファイル化します。これは、ロボットのベース位置を決める際の定量的な指針になります。

import numpy as np
import matplotlib.pyplot as plt

def forward_kinematics_3dof_spatial(theta1, theta2, theta3,
                                     l1, l2, l3):
    """3自由度空間アームの順運動学"""
    r_planar = (l2 * np.cos(theta2)
                + l3 * np.cos(theta2 + theta3))
    z = (l1
         + l2 * np.sin(theta2)
         + l3 * np.sin(theta2 + theta3))
    x = r_planar * np.cos(theta1)
    y = r_planar * np.sin(theta1)
    return x, y, z

# パラメータ
l1 = 0.3
l2 = 1.0
l3 = 0.7
N = 300000

# サンプリング
theta1 = np.random.uniform(0, 2*np.pi, N)
theta2 = np.random.uniform(-np.pi/2, np.pi, N)
theta3 = np.random.uniform(-np.pi, np.pi, N)

x, y, z = forward_kinematics_3dof_spatial(theta1, theta2, theta3,
                                            l1, l2, l3)

# 肩からの3D距離
r_3d = np.sqrt(x**2 + y**2 + (z - l1)**2)

# 手先の方向角を計算(簡略版: 仰角を n_elev 分割、方位角は軸対称で省略)
elevation = np.arctan2(z - l1, np.sqrt(x**2 + y**2))
n_elev_bins = 6
elev_bins = np.linspace(-np.pi/2, np.pi/2, n_elev_bins + 1)
i_elev = np.digitize(elevation, elev_bins) - 1
i_elev = np.clip(i_elev, 0, n_elev_bins - 1)

# 径方向ビン
r_bins = np.linspace(0, l2 + l3 + 0.1, 50)
r_centers = 0.5 * (r_bins[:-1] + r_bins[1:])

# 各径方向ビンでの方向カバレッジ
coverage = np.zeros(len(r_centers))
density = np.zeros(len(r_centers))

for j in range(len(r_centers)):
    mask = (r_3d >= r_bins[j]) & (r_3d < r_bins[j+1])
    density[j] = np.sum(mask)
    if density[j] > 0:
        unique_dirs = len(set(i_elev[mask]))
        coverage[j] = unique_dirs / n_elev_bins

# 可視化
fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)

# 上: サンプル密度
axes[0].bar(r_centers, density, width=r_bins[1]-r_bins[0],
            color='steelblue', alpha=0.7, edgecolor='black', linewidth=0.3)
axes[0].set_ylabel('Sample Count')
axes[0].set_title('Radial Profile of 3D Workspace')
axes[0].grid(True, alpha=0.3)
axes[0].axvline(x=l2+l3, color='red', linestyle='--',
                label=f'$l_2+l_3={l2+l3}$')
axes[0].axvline(x=abs(l2-l3), color='orange', linestyle='--',
                label=f'$|l_2-l_3|={abs(l2-l3)}$')
axes[0].legend()

# 下: 方向カバレッジ(リーチャビリティ指標)
axes[1].plot(r_centers, coverage, 'o-', color='magenta',
             markersize=4, linewidth=2)
axes[1].fill_between(r_centers, coverage, alpha=0.2, color='magenta')
axes[1].set_xlabel('Distance from shoulder $r$')
axes[1].set_ylabel('Direction Coverage')
axes[1].set_ylim(0, 1.1)
axes[1].set_title('Reachability (Direction Coverage) vs Distance')
axes[1].grid(True, alpha=0.3)
axes[1].axvline(x=l2+l3, color='red', linestyle='--')
axes[1].axvline(x=abs(l2-l3), color='orange', linestyle='--')

plt.tight_layout()
plt.savefig('reachability_radial_profile.png', dpi=150,
            bbox_inches='tight')
plt.show()

上段のサンプル密度プロファイルは、3D作業空間内で手先がどの距離に到達しやすいかを示しています。ピークは $r \approx 1.0$($l_2$ 付近)にあり、腕を適度に曲げた状態が最も多くの関節角の組み合わせで実現できることを裏付けています。$r = l_2 + l_3 = 1.7$(完全に伸ばした状態)や $r = |l_2 – l_3| = 0.3$(完全に折り畳んだ状態)の近傍では密度が急減しています。

下段の方向カバレッジは、各距離で何通りの仰角方向からアプローチできるかを示すリーチャビリティ指標です。中間距離($r \approx 0.5 \sim 1.2$)でカバレッジが最大になり、作業空間の端($r$ が小さすぎるか大きすぎる領域)でカバレッジが低下しています。

これらの結果から、ロボットアームの作業対象は肩から $l_2$ 程度の距離に配置するのが最適であることが定量的に示されます。宇宙ロボットのベース位置を決定する際、ターゲット(修理対象、把持対象など)がこの最適距離帯に入るように配置すると、最も高い操作性と冗長性が得られます。

まとめ

本記事では、ロボットアームの作業空間解析とリーチャビリティについて、理論からPython実装まで一貫して解説しました。

  • 作業空間とは、エンドエフェクタが到達可能な位置の集合であり、順運動学の値域として定義される
  • 到達可能作業空間(少なくとも1姿勢で届く)と器用作業空間(任意の姿勢で届く)の区別は、宇宙ロボットの設計で特に重要である
  • 解析的方法は2リンクアームの円環領域のように単純な構造で有効だが、多自由度ロボットではモンテカルロ法が実用的な代替手段となる
  • 関節リミットは作業空間を縮小させ、その影響パターンはリミットがかかる関節の種類(ベース旋回 vs 肘)によって質的に異なる
  • 障害物の影響は、手先位置だけでなく全リンクの干渉チェックが必要であり、有効作業空間は直感よりも大幅に狭くなることがある
  • リーチャビリティマップは、到達可能性の「質」を可視化する強力なツールであり、ロボットの設計最適化やタスクプランニングに不可欠である
  • モンテカルロ法で得られる点密度は到達の容易さを反映しており、中間距離($r \approx l_2$)で最も高い冗長性が得られる

本記事を含む一連の記事で、ロボティクスの運動学(Ch.1: 姿勢表現、DH パラメータ、順運動学、作業空間解析)の基礎を学びました。ここまでは「ロボットがどこに届くか」という幾何学的・運動学的な問題を扱ってきましたが、次は「ロボットをどう動かすか」という力学的な問題に進みます。

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