からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

【Python】円周上の点とx軸を結ぶ円弧を作図したい

はじめに

 調べても分からなかったので自分なりにやってみる黒魔術シリーズです。もっといい方法があれば教えてください。

 この記事では、円周上の点とx軸線上の点をいい感じに繋ぐ曲線のグラフを作成します。

【目次】

円周上の点とx軸を結ぶ円弧を作図したい

 Matplotlibライブラリを利用して、2次元空間における円周(circle)上の1点とx軸の正の部分を結ぶ良い感じの曲線を作成したい。また、その円弧(circular arc)の半径を調整して、2次元ベクトルのなす角(angle between two vectors)を示したい。
 度数法と弧度法の角度の関係や、角度と座標の関係については「円周の作図【Matplotlib】 - からっぽのしょこ」を参照のこと。

 利用するライブラリを読み込む。

# 利用ライブラリ
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation


なす角の描画

 まずは、完成図(やりたいこと)を確認する。

 2次元ベクトルと標準単位ベクトルのなす角を円弧(角マーク)で示すグラフを作成する。
 なす角については「【Python】3.4:ベクトルとx軸のなす角の可視化【『スタンフォード線形代数入門』のノート】 - からっぽのしょこ」を参照のこと。

 2次元ベクトル  \mathbf{x} を作成して、なす角を計算する。

# 2次元ベクトルを指定
x = np.array([-3.0, -2.0])

# ベクトルxとx軸のなす角(ラジアン)を計算
theta = np.arccos(x[0] / np.linalg.norm(x))
print(theta) # 0からπの値
print(theta / np.pi) # 0から1の値
2.5535900500422257
0.8128329581890013

 ベクトル  \mathbf{x} = (x_1, x_2) を1次元配列 x として値を指定する。Pythonではインデックスが0から割り当てられるので、 x_1x[0] に対応することに注意する。

  \mathbf{x} と標準単位ベクトル  \mathbf{e}_1 = (1, 0) のなす角  \theta は、次の式で計算できる。

 \displaystyle
\theta
    = \arccos \left(
          \frac{x_1}{\|\mathbf{x}\|}
      \right)

  \theta はラジアン(弧度法における角度)であり、 0 \leq \theta \leq \pi の値(度数法による角度  0^{\circ} \leq \theta^{\circ} \leq 180^{\circ} に対応)をとる。ただし、 \mathbf{x} が0ベクトルだと0除算になるため定義できない(計算結果が nan になる)。
 コサイン関数  \cos x の逆関数(逆コサイン関数)  \arccos xnp.arccos()、ユークリッドノルム  |\mathbf{x}|np.linalg.norm() で計算できる。

 2つのベクトル(線分)によって、劣角  0 \leq \theta \leq \pi と優角  2 \pi - \theta の2つの角(度数法だと劣角  0^{\circ} \leq \theta^{\circ} \leq 180^{\circ} と優角  360^{\circ} - \theta^{\circ} )ができる。
 なす角  \theta は劣角の角度に対応する。

 作図用の角度を計算する。

# 座標に応じて符号を変換
sgn_x   = 1.0 if x[1] >= 0.0 else -1.0
theta_x = sgn_x * theta
print(theta_x)
-2.5535900500422257

 作図用のなす角を  \theta_x で表すことにする。
 x軸線の正の部分(標準単位ベクトル)から左回りにできる角が優角のとき、言い換えると、 x_2 が負の値(ベクトル  \mathbf{x} が第3・4象限)のとき、右回りにできる劣角を負の角度  - \theta として扱う。
  x_2 が0以上の値(ベクトル  \mathbf{x} が第1・2象限)のとき、  \theta を用いる。

 \displaystyle
\theta_x
    = \begin{cases}
          - \theta &  (x_2 \lt 0) \\
          \theta & (x_2 \geq 0)
      \end{cases}

 ベクトル x のy軸の値 x[1] が、0 以上であれば sgn_x を正の符号 10 未満であれば負の符号 -1 にする。
 y座標の符号となす角の積を theta_x とする。

 角マークと角ラベルの座標を計算する。

# 角マーク用のラジアンを作成
t_vals = np.linspace(start=0.0, stop=sgn_x * theta, num=100)
print(t_vals[:5])

# 角マークの座標を計算
r = 0.3
angle_mark_arr = np.vstack(
    [r * np.cos(t_vals), 
     r * np.sin(t_vals)]
)
print(angle_mark_arr[:, :5])

# 角ラベルの座標を計算
r = 0.5
angle_label_vec = np.array(
    [r * np.cos(0.5 * theta_x), 
     r * np.sin(0.5 * theta_x)]
)
print(angle_label_vec)
[ 0.         -0.02579384 -0.05158768 -0.07738152 -0.10317536]
[[ 0.3         0.29990021  0.2996009   0.29910226  0.29840464]
 [ 0.         -0.00773729 -0.01546944 -0.02319129 -0.03089772]]
[ 0.14489207 -0.47854601]

  0 から  \theta_x までのラジアン  t を作成して、角マークの座標を計算する。

 角マークは、半径を  r として、次の式で円弧の座標(x軸・y軸の値)  (x, y) を計算する。

 \displaystyle
\left\{
    \begin{aligned}
        x  &= r \cos t \\
        y  &= r \sin t
    \end{aligned}
\right.

 半径 r で角マークのサイズ(原点からのノルム)を調整する。

 角ラベルは、円弧(なす角)の中点に配置するため、ノルムを  r として、次の式で座標を計算する。

 \displaystyle
\left\{
    \begin{aligned}
        x  &= r \cos \frac{\theta_x}{2} \\
        y  &= r \sin \frac{\theta_x}{2}
    \end{aligned}
\right.

 ノルム r で角ラベルの位置(原点からのノルム)を調整する。

 2次元空間上にベクトル  \mathbf{x} となす角のグラフを作成する。

# グラフサイズを設定
x_min = np.floor(np.min([0.0, x[0]])) - 1
x_max =  np.ceil(np.max([1.0, x[0]])) + 1
y_min = np.floor(np.min([0.0, x[1]])) - 1
y_max =  np.ceil(np.max([1.0, x[1]])) + 1

# 2Dベクトルのなす角を作図
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
ax.plot([0, 1], [0, 0], 
        color='black', linewidth=2, 
        label='$e_1 = (1, 0)$') # 標準単位ベクトルe1
ax.quiver(0, 0, *x, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1, 
          label='$x = ({}, {})$'.format(*x)) # ベクトルx
ax.plot(*angle_mark_arr, 
        color='black', linewidth=1) # 角マーク
ax.text(*angle_label_vec, 
        s='$\\theta$', size=15, ha='center', va='center') # 角ラベル
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('$\\theta = {:.2f}'.format(theta / np.pi)+'\pi, ' + 
             '\\theta^\circ = {:.1f}'.format(theta * 180.0/np.pi)+'^\circ$', loc='left')
fig.suptitle('$\\theta = \\arccos(\\frac{x_1}{\|x\|})$', fontsize=20)
ax.grid()
ax.legend()
ax.set_aspect('equal')
plt.show()

ベクトルとx軸のなす角のグラフ

 plt.quiver() でベクトルを描画する。第1・2引数に始点(原点)の座標、第3・4引数に移動量(ベクトル)を指定する。
 配列 x の頭に * を付けてアンパック(展開)して指定している。

 標準単位ベクトル  \mathbf{e}_1plt.plot() で描画する。第1引数に始点と終点のx軸座標、第2引数にy軸座標を指定する。

 ベクトルの値を変化させたアニメーションを作成する。

・作図コード(クリックで展開)

# フレーム数を指定
frame_num = 60

# 変化するベクトルを作成
beta = 1.5
rad_n = np.linspace(start=0.0, stop=2.0*np.pi, num=frame_num+1)[:frame_num]
x_n = np.array(
    [beta * np.cos(rad_n), beta * np.sin(rad_n)]
).T

# グラフサイズを設定
x_min = np.floor(np.min([0.0, *x_n[:, 0]]))
x_max =  np.ceil(np.max([1.0, *x_n[:, 0]]))
y_min = np.floor(np.min([0.0, *x_n[:, 1]]))
y_max =  np.ceil(np.max([1.0, *x_n[:, 1]]))

# グラフオブジェクトを初期化
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
fig.suptitle('$\\theta = \\arccos(\\frac{x_1}{\|x\|})$', fontsize=20)

# 作図処理を関数として定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目のベクトルを取得
    x = x_n[i]
    
    # ベクトルxとx軸のなす角(ラジアン)を計算
    theta = np.arccos(x[0] / np.linalg.norm(x))

    # 座標に応じて符号を変換
    sgn_x   = 1.0 if x[1] >= 0.0 else -1.0
    theta_x = sgn_x * theta

    # 角マーク用のラジアンを作成
    t_vals = np.linspace(start=0.0, stop=theta_x, num=100)

    # 角マークの座標を計算
    r = 0.3
    angle_mark_arr = np.vstack(
        [r * np.cos(t_vals), 
         r * np.sin(t_vals)]
    )

    # 角ラベルの座標を計算
    r = 0.45
    angle_label_vec = np.array(
        [r * np.cos(0.5 * theta_x), 
         r * np.sin(0.5 * theta_x)]
    )
    
    # 2Dベクトルのなす角を作図
    ax.plot([0, 1], [0, 0], 
            color='black', linewidth=2, 
            label='$e_1 = (1, 0)$') # 標準単位ベクトルe1
    ax.quiver(0, 0, *x, 
              color='red', units='dots', width=3, headwidth=5, 
              angles='xy', scale_units='xy', scale=1, 
              label='$x = ({: .2f}, {: .2f})$'.format(*x)) # ベクトルx
    ax.plot(*angle_mark_arr, 
            color='black', linewidth=1) # 角マーク
    ax.text(*angle_label_vec, 
            s='$\\theta$', size=15, ha='center', va='center') # 角ラベル
    ax.set_xticks(ticks=np.arange(x_min, x_max+1))
    ax.set_yticks(ticks=np.arange(y_min, y_max+1))
    ax.set_xlabel('$x_1$')
    ax.set_ylabel('$x_2$')
    ax.set_title('$\\theta = {:.2f}'.format(theta / np.pi)+'\pi, ' + 
                 '\\theta^\circ = {:.1f}'.format(theta * 180.0/np.pi)+'^\circ$', loc='left')
    ax.grid()
    ax.legend(loc='upper left')
    ax.set_aspect('equal')

# gif画像を作成
ani = FuncAnimation(fig=fig, func=update, frames=frame_num, interval=100)

# gif画像を保存
ani.save(filename='2d_angle_x.gif')

 作図処理をupdate()として定義して、FuncAnimation()でgif画像を作成します。

ベクトルとなす角の関係


円弧の描画

 2次元ベクトルとx軸線を繋ぐ円弧(角マーク)の座標計算とノルム操作をグラフで確認する。

 2次元ベクトル  \mathbf{x} を作成して、ノルムを計算する。

# 2次元ベクトルを指定
x = np.array([-3.0, -2.0])

# ノルムを計算
norm_x = np.linalg.norm(x)
print(norm_x)
3.605551275463989

 ベクトル  \mathbf{x} = (x_1, x_2) を1次元配列 x として値を指定する。
  n 次元ベクトル  \mathbf{x} のユークリッドノルム

 \displaystyle
\|\mathbf{x}\|
    = \sqrt{\mathbf{x} \cdot \mathbf{x}}
    = \sqrt{\sum_{i=1}^n x_i^2}

np.linalg.norm() で計算する。
 x軸方向の標準単位ベクトルを  \mathbf{e}_1 = (1, 0) とする。標準単位ベクトルのノルムは次元に関わらず  \|\mathbf{e}_i\| = 1 である。

 ベクトル  \mathbf{x}, \mathbf{e}_1 をグラフで確認する。 

# グラフサイズを設定
x_min = np.floor(np.min([0.0, x[0]])) - 1
x_max =  np.ceil(np.max([1.0, x[0]])) + 1
y_min = np.floor(np.min([0.0, x[1]])) - 1
y_max =  np.ceil(np.max([1.0, x[1]])) + 1

# 2Dベクトルを作図
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
ax.scatter(0, 0, 
           color='orange', s=100, label='$O = (0, 0)$') # 原点
ax.scatter(1, 0, 
           color='black', s=100, label='$e_1 = (1, 0)$') # x軸線上の点
ax.scatter(*x, 
           color='red', s=100, label='$x = ({}, {})$'.format(*x)) # 点x
ax.plot([0, 1], [0, 0], 
        color='black', linewidth=2) # 標準単位ベクトルe1
ax.quiver(0, 0, *x, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルx
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('$\|e_1\| = {}, \|x\| = {:.2f}$'.format(np.linalg.norm([1, 0]), norm_x), loc='left')
fig.suptitle('$x = (x_1, x_2)$', fontsize=20)
ax.grid()
ax.legend()
ax.set_aspect('equal')
plt.show()

ベクトルとノルム

 ベクトル  \mathbf{x} のノルム  \|\mathbf{x}\| は、原点  \mathbf{O} = (0, 0) と点  \mathbf{x} の距離に対応する。

 ここから、円周上の2点を考える。

 半径を指定して、円周の座標を計算する。

# 半径(ノルム)を指定
r = 2.5

# 円周用のラジアンを作成
t_vals = np.linspace(start=0.0, stop=2.0*np.pi, num=100)
print(t_vals[:5])

# 円周の座標を計算
circle_x_vals = r * np.cos(t_vals)
circle_y_vals = r * np.sin(t_vals)
print(circle_x_vals[:5])
print(circle_y_vals[:5])
print(np.linalg.norm(np.vstack([circle_x_vals[:5], circle_y_vals[:5]]), axis=0))
[0.         0.06346652 0.12693304 0.19039955 0.25386607]
[2.5        2.49496669 2.47988703 2.45482174 2.41987175]
[0.         0.1585598  0.31648113 0.47312811 0.62786997]
[2.5 2.5 2.5 2.5 2.5]

 原点を中心とする円周の座標  \mathbf{z} = (z_1, z_2) は、半径を  r として、 0 から  2 \pi までのラジアン  t を用いて、次の式で計算できる。

 \displaystyle
\left\{
    \begin{aligned}
        z_1
           &= r \cos t \\
        z_2
           &= r \sin t
    \end{aligned}
\right.

 circle_*_vals の同じインデックスの要素が、円周上の各点の座標に対応する。
 円周上の点のノルム(中心との距離)は半径  \|\mathbf{z}\| = r になる。

 ベクトルのノルムを変更する。

# ベクトルのノルムを調整
tilde_x = r * x / np.linalg.norm(x)
print(tilde_x)
print(np.linalg.norm(tilde_x))
[-2.08012574 -1.38675049]
2.5000000000000004

  \mathbf{x} をノルムで割るとノルムが1のベクトルになり、さらに  r を掛けるとノルムが  r のベクトルになる。

 \displaystyle
\tilde{\mathbf{x}}
    = r \frac{\mathbf{x}}{\|\mathbf{x}\|}
,\ 
\|\tilde{\mathbf{x}}\|
    = r

 ベクトル  \mathbf{x} (の延長線)と円周の交点  \tilde{\mathbf{x}} をグラフで確認する。 

# グラフサイズを設定
x_min = np.floor(np.min([-r, x[0]])) - 1
x_max =  np.ceil(np.max([r, x[0]])) + 1
y_min = np.floor(np.min([-r, x[1]])) - 1
y_max =  np.ceil(np.max([r, x[1]])) + 1

# 2Dベクトルを結ぶ円周を作図
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
ax.plot(circle_x_vals, circle_y_vals, 
        color='black', linewidth=1.5) # 円周
ax.scatter(0, 0, 
           color='orange', s=100, label='$O = (0, 0)$') # 原点
ax.scatter(r, 0, 
           color='black', s=100, label='$r\ e_1 = ('+str(r)+', 0)$') # x軸線上の点
ax.scatter(*tilde_x, 
           color='red', s=100, 
           label='$\\tilde{x} = [tex: '+'$({:.2f}, {:.2f})$'.format(*tilde_x)) # 点x
ax.plot([0, r], [0, 0], 
        color='black', linewidth=2) # x軸線
ax.quiver(0, 0, *x, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルx
if norm_x < r:
    ax.plot([x[0], tilde_x[0]], [x[1], tilde_x[1]], 
            color='red', linewidth=2, linestyle=':') # ベクトルxの延長線
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('$r = '+str(r)+', ' +
             '\|\\tilde{x}\| = '+'{:.2f}$'.format(np.linalg.norm(tilde_x)), loc='left')
fig.suptitle('$\\tilde{x} = r \\frac{x}{\|x\|}$', fontsize=20)
ax.grid()
ax.legend(loc='upper right')
ax.set_aspect('equal')
plt.show()

ベクトル(の延長線)上と円周上の点

 「半径  r の円周」は中心からの距離が  r の点の集合なので、「ノルムが  r のベクトル  \tilde{\mathbf{x}}」は円周上の点であり、円周とベクトル  \mathbf{x} (の延長線)の交点になる。

 円周上の2点が得られた。続いて、2点を結ぶ円弧を考える。

 ベクトル  \tilde{\mathbf{x}}, \mathbf{e}_1 のなす角を計算する。

# ベクトルxとx軸のなす角(ラジアン)を計算
theta = np.arccos(x[0] / norm_x)
print(theta)

# 円周上の点を作成
p = np.array(
    [r * np.cos(theta), r * np.sin(theta)]
)
print(p)
print(np.linalg.norm(p))
2.5535900500422257
[-2.08012574  1.38675049]
2.5

  \mathbf{x}, \mathbf{e}_1 のなす角  \theta は、次の式で計算できる。

 \displaystyle
\theta
    = \arccos \left(
          \frac{
              \mathbf{x} \cdot \mathbf{e}_1
          }{
              \|\mathbf{x}\| \|\mathbf{e}_1\|
          }
      \right)
    = \arccos \left(
          \frac{x_1}{\|\mathbf{x}\|}
      \right)

 ラジアン  \theta を用いて、円周上の点  \mathbf{p} = (p_1, p_2) の座標を計算する。

 \displaystyle
\left\{
    \begin{aligned}
        p_1
           &= r \cos \theta \\
        p_2
           &= r \sin \theta
    \end{aligned}
\right.
,\ 
\|\mathbf{p}\|
    = r

 円周の座標と同様に、ノルムは半径になる。

  \tilde{\mathbf{x}}, r \mathbf{e}_1 となす角  \theta の関係をグラフで確認する。

# ラベル位置の調整値を指定
d = 1.2

# x軸の値を計算
c1 = r * np.cos(theta)

# 2Dベクトルを結ぶ円周を作図
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
ax.plot(circle_x_vals, circle_y_vals, 
        color='black', linewidth=1.5) # 円周
ax.scatter(r, 0, 
           color='black', s=100, label='$r\ e_1 = (r, 0)$') # x軸線上の点
ax.scatter(c1, 0, 
           facecolor='white', edgecolor='blue', s=100, linestyle='--', 
           label='$(r\ \cos \\theta, 0)$') # 点cosθ
ax.scatter(*p, 
           facecolor='white', edgecolor='red', s=100, linestyle='--', 
           label='$p = ({:.2f}, {:.2f})$'.format(*p)) # 点p
ax.scatter(*tilde_x, 
           color='red', s=100, 
           label='$\\tilde{x} = [tex: '+'$({:.2f}, {:.2f})$'.format(*tilde_x)) # 点x
ax.plot([0, r], [0, 0], 
        color='black', linewidth=2) # x軸線
ax.plot([0, c1], [0, 0], 
        color='blue', linewidth=1.5, linestyle='--') # 補助線cosθ
ax.plot([c1, tilde_x[0]], [0, tilde_x[1]], 
        color='black', linewidth=1.5, linestyle=':') # 補助線x
if x[1] < 0.0:
    ax.plot([c1, p[0]], [0, p[1]], 
            color='black', linewidth=1.5, linestyle=':') # 補助線p
    ax.plot([0, p[0]], [0, p[1]], 
            color='red', linewidth=2, linestyle='--') # ベクトルp
    ax.text(*d*tilde_x, 
            s='$- \\theta$', size=15, ha='center', va='center') # ラジアンラベルx
ax.quiver(0, 0, *x, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルx
if norm_x < r:
    ax.plot([x[0], tilde_x[0]], [x[1], tilde_x[1]], 
            color='red', linewidth=2, linestyle=':') # ベクトルxの延長線
ax.text(*d*p, 
        s='$\\theta$', size=15, ha='center', va='center') # ラジアンラベルp
ax.text(d*r, 0, 
        s='$0$', size=15, ha='center', va='center') # ラジアンラベルe1
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('$r = {}, \\theta = {:.2f} \pi$'.format(r, theta/np.pi), loc='left')
fig.suptitle('$\\theta = \\arccos(\\frac{x_1}{\|x\|})$', fontsize=20)
ax.grid()
ax.legend(loc='upper right')
ax.set_aspect('equal')
plt.show()

円周上の点となす角の関係

 x軸線の正の部分から左回りに  \theta 移動した点の座標は  (r \cos \theta, r \sin \theta) である。
 なす角は  0 \leq \theta \leq \pi なので、 \mathbf{x} のy軸の値が正  x_2 \geq 0 のとき  x_2 = r \sin \theta、負  x_2 \lt 0 のとき  x_2 = - r \sin \theta となる。

 ベクトルの座標によって、 \theta の移動方向(右回り・左回り)が変わる。

 座標に応じて、円弧の座標を計算する。

# 円弧用のラジアンを作成
if x[1] >= 0.0:
    u_vals = np.linspace(start=0.0, stop=theta, num=100)
else:
    u_vals = np.linspace(start=-theta, stop=0.0, num=100)
print(u_vals[:5])

# 円弧の座標を計算
arc_x_vals = r * np.cos(u_vals)
arc_y_vals = r * np.sin(u_vals)
print(arc_x_vals[:5])
print(arc_y_vals[:5])
print(np.linalg.norm(np.vstack([arc_x_vals[:5], arc_y_vals[:5]]), axis=0))
[-2.55359005 -2.52779621 -2.50200237 -2.47620853 -2.45041469]
[-2.08012574 -2.04366814 -2.00585093 -1.96669926 -1.92623916]
[-1.38675049 -1.43993768 -1.49216689 -1.54340339 -1.59361309]
[2.5 2.5 2.5 2.5 2.5]

 原点を中心とする半径が  r の円弧の座標  \mathbf{z} = (z_1, z_2) は、 \mathbf{x} のy軸座標が「正  x_2 \geq 0 のとき  0 から  \theta まで」、「負  x_2 \lt 0 のとき  - \theta から  0 まで」のラジアン  u を用いて、次の式で計算できる。

 \displaystyle
\left\{
    \begin{aligned}
        z_1
           &= r \cos u \\
        z_2
           &= r \sin u
    \end{aligned}
\right.
,\ 
\|\mathbf{z}\|
    = r

 円周の座標と同様に、ノルムは半径になる。

 点  \tilde{\mathbf{x}}, r \mathbf{e}_1 を結ぶ半径  r の円弧を描画する。

# y座標の符号を取得
sgn_y = 1.0 if x[1] >= 0.0 else -1.0

# 2Dベクトルを結ぶ円弧を作図
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
ax.plot(circle_x_vals, circle_y_vals, 
        color='black', linewidth=1.5, linestyle=':') # 円周
ax.plot(arc_x_vals, arc_y_vals, 
        color='black', linewidth=2) # 円弧
ax.scatter(r, 0, 
           color='black', s=100, label='$r\ e_1 = (r, 0)$') # x軸線上の点
ax.scatter(*tilde_x, 
           color='red', s=100, 
           label='$\\tilde{x} = [tex: '+'$({:.2f}, {:.2f})$'.format(*tilde_x)) # 点x
ax.plot([0, r], [0, 0], 
        color='black', linewidth=2) # x軸線
ax.quiver(0, 0, *x, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルx
if norm_x < r:
    ax.plot([x[0], tilde_x[0]], [x[1], tilde_x[1]], 
            color='red', linewidth=2, linestyle=':') # ベクトルxの延長線
ax.text(d*r, 0, 
        s='$0$', size=15, ha='center', va='center') # ラジアンラベルe1
ax.text(*d*tilde_x, 
        s='$\\theta$', size=15, ha='center', va='center') # なす角ラベル
ax.set_xticks(ticks=np.arange(x_min, x_max+1))
ax.set_yticks(ticks=np.arange(y_min, y_max+1))
ax.set_xlabel('$x_1$')
ax.set_ylabel('$x_2$')
ax.set_title('$x = ({}, {}), r = {}'.format(*x, r)+', ' + 
             '\\theta = {:.2f} \pi$'.format(sgn_y*theta/np.pi), loc='left')
fig.suptitle('$\\theta = \mathrm{sgn(x_2)} \\arccos(\\frac{x_1}{\|x\|})$', fontsize=20)
ax.grid()
ax.legend()
ax.set_aspect('equal')
plt.show()

円周上の点を結ぶ円弧

 変数  x の符号を返す関数を  \mathrm{sgn}(x) とする(正確には符号関数は0のとき0になるが、この例では0のとき正の値とする)。

 \displaystyle
\mathrm{sgn}(x)
    = \begin{cases}
          -1 & (x \lt 0) \\
          1  & (x \geq 0) \\
      \end{cases}

 円座標  (r, \theta) は、符号関数を用いて、次の式で求められる。

 \displaystyle
\left\{
    \begin{aligned}
        r
           &= \|\mathbf{x}\|
        \\
        \theta
           &= \mathrm{sgn}(x_2)
              \arccos \left(
                  \frac{x_1}{\|\mathbf{x}\|}
              \right)
    \end{aligned}
\right.

 詳しくは、極座標系を参照のこと。

 以上で、円周上の2点を結ぶ円弧が得られた。

 ベクトルの値を変化させたアニメーションを作成する。

・作図コード(クリックで展開)

# フレーム数を指定
frame_num = 60

# 変化するベクトルを作成
beta = 4.0
rad_n = np.linspace(start=0.0, stop=2.0*np.pi, num=frame_num+1)[:frame_num]
x_n = np.array(
    [beta * np.cos(rad_n), beta * np.sin(rad_n)]
).T

# 半径を指定
r = 2.5

# 円周の座標を計算
t_vals = np.linspace(start=0.0, stop=2.0*np.pi, num=100)
circle_x_vals = r * np.cos(t_vals)
circle_y_vals = r * np.sin(t_vals)

# グラフサイズを設定
axis_size = np.ceil(np.max([r, abs(beta)])) + 1

# グラフオブジェクトを初期化
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
fig.suptitle('$\\theta = \\arccos(\\frac{x_1}{\|x\|})$', fontsize=20)

# 作図処理を関数として定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目のベクトルを取得
    x = x_n[i]
    
    # ノルムを調整
    tilde_x = r * x / np.linalg.norm(x)
    
    # ベクトルxとx軸のなす角(ラジアン)を計算
    theta = np.arccos(x[0] / np.linalg.norm(x))
    
    # 円周上の点を作成
    p = np.array([r * np.cos(theta), r * np.sin(theta)])
    
    # 円弧の座標を計算
    theta_x = theta if x[1] >= 0.0 else -theta
    u_vals = np.linspace(start=0.0, stop=theta_x, num=100)
    arc_x_vals = r * np.cos(u_vals)
    arc_y_vals = r * np.sin(u_vals)
    
    # 2Dベクトルを結ぶ円弧を作図
    d = 1.2
    ax.plot(circle_x_vals, circle_y_vals, 
            color='black', linewidth=1.5, linestyle=':') # 円周
    ax.plot(arc_x_vals, arc_y_vals, 
            color='black', linewidth=2) # 円弧
    ax.scatter(r, 0, 
               color='black', s=100, label='$r\ e_1 = (r, 0)$') # x軸線上の点
    ax.scatter(*p, 
               facecolor='white', edgecolor='red', s=100, linestyle='--', 
               label='$p = ({: .2f}, {: .2f})$'.format(*p)) # 点p
    ax.scatter(*tilde_x, 
               color='red', s=100, 
               label='$\\tilde{x} = [tex: '+'$({: .2f}, {: .2f})$'.format(*tilde_x)) # 点x
    ax.plot([0, r], [0, 0], 
            color='black', linewidth=2) # x軸線
    if x[1] < 0.0:
        ax.plot([tilde_x[0], p[0]], [tilde_x[1], p[1]], 
                color='black', linewidth=1.5, linestyle=':') # 補助線xp
        ax.plot([0, p[0]], [0, p[1]], 
                color='red', linewidth=2, linestyle='--') # ベクトルp
        ax.text(*d*tilde_x, 
                s='$- \\theta$', size=15, ha='center', va='center') # ラジアンラベルx
    ax.quiver(0, 0, *x, 
              color='red', units='dots', width=3, headwidth=5, 
              angles='xy', scale_units='xy', scale=1) # ベクトルx
    if np.linalg.norm(x) < r:
        ax.plot([x[0], tilde_x[0]], [x[1], tilde_x[1]], 
                color='red', linewidth=2, linestyle=':') # ベクトルxの延長線
    ax.text(*d*p, 
            s='$\\theta$', size=15, ha='center', va='center') # ラジアンラベルp
    ax.text(d*r, 0, 
            s='$0$', size=15, ha='center', va='center') # ラジアンラベルe1
    ax.set_xticks(ticks=np.arange(-axis_size, axis_size+1))
    ax.set_yticks(ticks=np.arange(-axis_size, axis_size+1))
    ax.set_xlabel('$x_1$')
    ax.set_ylabel('$x_2$')
    ax.set_title('$x = ({: .2f}, {: .2f})'.format(*x)+', ' + 
                 'r = {}'+str(r)+', ' + 
                 '\\theta = {:.2f} \pi'.format(theta/np.pi)+'$', loc='left')
    fig.suptitle('$\\theta = \\arccos(\\frac{x_1}{\|x\|})$', fontsize=20)
    ax.grid()
    ax.legend(loc='upper left')
    ax.set_aspect('equal')

# gif画像を作成
ani = FuncAnimation(fig=fig, func=update, frames=frame_num, interval=100)

# gif画像を保存
ani.save(filename='2d_arc_x.gif')

ベクトルと円周上の点を結ぶ円弧の関係

 ベクトル  \mathbf{x} のy座標の符号(正負)によって円弧を描画する向きが変わるのが分かる。

 この記事では、2次元空間における1つの線分を結ぶ曲線を扱った。次の記事では、2つの線分を結ぶ曲線を扱う。

おわりに

 なんとも解説するのが面倒で書きかけで放置してしまってました。たしかスタンフォード線形代数シリーズの5章の補足として書き始めたのですが、放置したまま6章が書き終わりました。原型は3章の時点で書けてたはずなのに。放置したままでずっと気持ち悪かったので、いいかげん書き切ろう再開しましたがやっぱり筆が重かったです。
 なんでこんなに説明が大変なんだって、1つの記事に内容を詰め込みすぎなんだと気付きました。説明の説明をすると、話が入り組むし構成がややこしくなるのも当然ですね。というわけで、書きたかったメインの内容は次の記事で、この記事はその内容を説明するための説明で、そもそもが本筋の説明でもあるみたいな感じです。この説明だけでもうメンドいね。

 他の記事との統一感と1記事での読み応えとか、検索のされやすさと管理のしやすさとか、記事の粒度って意外と難しくてずっと悩んでます。この記事も2つの内容を書いてる気がします。

 2023年8月4日は、こぶしファクトリーの元リーダーの広瀬彩海さんの24歳お誕生です。

 こぶしの曲も最高なのでぜひ聴いてください。

【次の内容】

www.anarchive-beta.com