からっぽのしょこ

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

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

はじめに

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

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

【前の内容】

www.anarchive-beta.com

【目次】

円周上の2点を結ぶ円弧を作図したい

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

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

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


なす角の描画

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

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

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

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

# ベクトルa,bのなす角(ラジアン)を計算
cos_theta = np.dot(a, b) / np.linalg.norm(a) / np.linalg.norm(b)
theta = np.arccos(cos_theta)
print(theta) # 0からπの値
print(theta / np.pi) # 0から1の値
2.819842099193151
0.8975836176504333

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

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

 \displaystyle
\theta
    = \arccos \left(
          \frac{
              \mathbf{a} \cdot \mathbf{b}
          }{
              \|\mathbf{a}\| \|\mathbf{b}\|
          }
      \right)

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

  \mathbf{a}, \mathbf{b} それぞれのなす角を計算する。

# ベクトルaとx軸のなす角(ラジアン)を計算
sgn_a   = 1.0 if a[1] >= 0.0 else -1.0
theta_a = sgn_a * np.arccos(a[0] / np.linalg.norm(a))
print(theta_a / np.pi)

# ベクトルbとx軸のなす角(ラジアン)を計算
sgn_b   = 1.0 if b[1] >= 0.0 else -1.0
theta_b = sgn_b * np.arccos(b[0] / np.linalg.norm(b))
print(theta_b / np.pi)

# 角度の差に応じて負の角度を優角に変換
if abs(theta_a - theta_b) > np.pi:
    theta_a = theta_a if a[1] >= 0.0 else theta_a + 2.0*np.pi
    theta_b = theta_b if b[1] >= 0.0 else theta_b + 2.0*np.pi
print(theta_a / np.pi)
print(theta_b / np.pi)
0.1475836176504333
-0.75
0.1475836176504333
-0.75

 ベクトル  \mathbf{a}, \mathbf{b} それぞれと標準単位ベクトル(x軸の正の部分)  \mathbf{e}_1 = (1, 0) のなす角を  \theta_a, \theta_b で表し、次の式で計算する。

 \displaystyle
\begin{aligned}
\mathrm{sgn}(x)
   &= \begin{cases}
          -1 & (x \lt 0) \\
          1  & (x \geq 0) \\
      \end{cases}
\\
\theta_x
   &= \mathrm{sgn}(x_2)
      \arccos \left(
          \frac{x_1}{\|\mathbf{x}\|}
      \right)
\end{aligned}

 x軸線の正の部分から左回りにできる角が優角のとき(  x_2 が負の値のとき)、右回りにできる劣角を負の角度  - \theta として扱う。
 この操作を、符号関数  \mathrm{sgn}(x) を用いて数式で表している(正確には符号関数は0のとき0になるが、この例では0のとき正の値とする)。
 詳しくは「【Python】円周上の点とx軸を結ぶ円弧を作図したい - からっぽのしょこ」を参照のこと。

 ただし、 \theta_a, \theta_b の差が優角になる場合(一方が正の角度でもう一方が負の角度で、差の絶対値  |\theta_a - \theta_b| \pi を超える場合)は、負の角度の方に  2 \pi を足して優角に変換する。

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

# 角マーク用のラジアンを作成
t_vals = np.linspace(start=theta_a, stop=theta_b, 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, :5])

# 角ラベルの座標を計算
r = 0.5
angle_label_vec = np.array(
    [r * np.cos(np.median(t_vals)), 
     r * np.sin(np.median(t_vals))]
)
print(angle_label_vec)
[0.46364761 0.43516436 0.4066811  0.37819785 0.34971459]
[[0.26832816 0.27204023 0.27553161 0.27879947 0.28184116]
 [0.13416408 0.12646783 0.11866899 0.11077388 0.10278891]]
[ 0.29235514 -0.40562109]

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

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

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

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

 角ラベルは、円弧(なす角)の中点に配置する。
 r で角ラベルの位置(原点からのノルム)を調整する。

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

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

# 2Dベクトルのなす角を作図
fig, ax = plt.subplots(figsize=(6, 6), facecolor='white')
ax.quiver(0, 0, *a, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1, 
          label='$a = ({}, {}'.format(*a)+')$') # ベクトルa
ax.quiver(0, 0, *b, 
          color='blue', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1, 
          label='$b =({}, {}'.format(*b)+')$') # ベクトルb
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{a \cdot b}{\|a\| \|b\|})$', fontsize=20)
ax.grid()
ax.legend()
ax.set_aspect('equal')
plt.show()

2つのベクトルのなす角のグラフ

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

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

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

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

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

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

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

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

    # ベクトルa・bとx軸のなす角(ラジアン)を計算
    sgn_a   = 1.0 if a[1] >= 0.0 else -1.0
    theta_a = sgn_a * np.arccos(a[0] / np.linalg.norm(a))
    sgn_b   = 1.0 if b[1] >= 0.0 else -1.0
    theta_b = sgn_b * np.arccos(b[0] / np.linalg.norm(b))

    # 角度の差に応じて負の角度を優角に変換
    if abs(theta_a - theta_b) > np.pi:
        theta_a = theta_a if a[1] >= 0.0 else theta_a + 2.0*np.pi
        theta_b = theta_b if b[1] >= 0.0 else theta_b + 2.0*np.pi

    # 角マーク用のラジアンを作成
    t_vals = np.linspace(start=theta_a, stop=theta_b, 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(np.median(t_vals)), 
         r * np.sin(np.median(t_vals))]
    )
    
    # 2Dベクトルのなす角を作図
    ax.quiver(0, 0, *a, 
              color='red', units='dots', width=3, headwidth=5, 
              angles='xy', scale_units='xy', scale=1, 
              label='$a = ({: .2f}, {: .2f}'.format(*a)+')$') # ベクトルa
    ax.quiver(0, 0, *b, 
              color='blue', units='dots', width=3, headwidth=5, 
              angles='xy', scale_units='xy', scale=1, 
              label='$b =({: .2f}, {: .2f}'.format(*b)+')$') # ベクトルb
    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_ab.gif')

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

ベクトルとなす角の関係


円弧の描画

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

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

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

# ノルムを計算
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
print(norm_a)
print(norm_b)
3.605551275463989
1.4142135623730951

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

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

np.linalg.norm() で計算する。

 ベクトル  \mathbf{a}, \mathbf{b} をグラフで確認する。 

# グラフサイズを設定
x_min = np.floor(np.min([0.0, a[0], b[0]])) - 1
x_max =  np.ceil(np.max([1.0, a[0], b[0]])) + 1
y_min = np.floor(np.min([0.0, a[1], b[1]])) - 1
y_max =  np.ceil(np.max([1.0, a[1], b[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(*a, 
           color='red', s=100, label='$a = ({}, {})$'.format(*a)) # 点a
ax.scatter(*b, 
           color='blue', s=100, label='$b = ({}, {})$'.format(*b)) # 点b
ax.quiver(0, 0, *a, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルa
ax.quiver(0, 0, *b, 
          color='blue', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルb
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('$\|a\| = {:.2f}, \|b\| = {:.2f}$'.format(norm_a, norm_b), 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_a = r * a / np.linalg.norm(a)
tilde_b = r * b / np.linalg.norm(b)
print(tilde_a)
print(np.linalg.norm(tilde_a))
print(tilde_b)
print(np.linalg.norm(tilde_b))
[-2.08012574 -1.38675049]
2.5000000000000004
[-1.76776695  1.76776695]
2.5

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

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

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

# グラフサイズを設定
x_min = np.floor(np.min([-r, a[0], b[0]])) - 1
x_max =  np.ceil(np.max([r, a[0], b[0]])) + 1
y_min = np.floor(np.min([-r, a[1], b[1]])) - 1
y_max =  np.ceil(np.max([r, a[1], b[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(*tilde_a, 
           color='red', s=100, 
           label='$\\tilde{a} = '+'({:.2f}, {:.2f})$'.format(*tilde_a)) # 点a
ax.scatter(*tilde_b, 
           color='blue', s=100, 
           label='$\\tilde{b} = '+'({:.2f}, {:.2f})$'.format(*tilde_b)) # 点b
ax.quiver(0, 0, *a, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルa
if norm_a < r:
    ax.plot([a[0], tilde_a[0]], [a[1], tilde_a[1]], 
            color='red', linewidth=2, linestyle=':') # ベクトルaの延長線
ax.quiver(0, 0, *b, 
          color='blue', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルb
if norm_b < r:
    ax.plot([b[0], tilde_b[0]], [b[1], tilde_b[1]], 
            color='blue', linewidth=2, linestyle=':') # ベクトルbの延長線
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{a}\| = '+'{:.2f}'.format(np.linalg.norm(tilde_a))+', ' + 
             '\|\\tilde{b}\| = '+'{:.2f}$'.format(np.linalg.norm(tilde_b)), loc='left')
fig.suptitle('$\\tilde{x} = r \\frac{x}{\|x\|}$', fontsize=20)
ax.grid()
ax.legend()
ax.set_aspect('equal')
plt.show()

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

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

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

 ベクトル  \tilde{\mathbf{a}}, \tilde{\mathbf{b}} それぞれと  \mathbf{e}_1 のなす角を計算する。

# ベクトルaとx軸のなす角(ラジアン)を計算
sgn_a   = 1.0 if a[1] >= 0.0 else -1.0
theta_a = sgn_a * np.arccos(a[0] / np.linalg.norm(a))
print(theta_a)

# ベクトルbとx軸のなす角(ラジアン)を計算
sgn_b   = 1.0 if b[1] >= 0.0 else -1.0
theta_b = sgn_b * np.arccos(b[0] / np.linalg.norm(b))
print(theta_b)
-2.5535900500422257
2.356194490192345

 ベクトル  \mathbf{x} と標準単位ベクトル  \mathbf{e}_1 = (1, 0) の円座標  (r, \theta) における角度(なす角)  \theta は、符号関数  \mathrm{sgn}(x) を用いて、次の式で計算できる。

 \displaystyle
\begin{aligned}
\mathrm{sgn}(x)
   &= \begin{cases}
          -1 & (x \lt 0) \\
          1  & (x \geq 0) \\
      \end{cases}
\\
\theta
   &= \mathrm{sgn}(x_2)
      \arccos \left(
          \frac{x_1}{\|\mathbf{x}\|}
      \right)
\end{aligned}

 ベクトル  \tilde{\mathbf{a}}, \tilde{\mathbf{b}} それぞれの角度に対する円弧の座標を計算する。

# 円弧用のラジアンを作成
t_a_vals = np.linspace(start=0.0, stop=theta_a, num=100)
t_b_vals = np.linspace(start=0.0, stop=theta_b, num=100)
print(t_a_vals[:5])
print(t_b_vals[:5])

# なす角aの円弧の座標を計算
arc_a_x_vals = r * np.cos(t_a_vals)
arc_a_y_vals = r * np.sin(t_a_vals)
print(arc_a_x_vals[:5])
print(arc_a_y_vals[:5])
print(np.linalg.norm(np.vstack([arc_a_x_vals[:5], arc_a_y_vals[:5]]), axis=0))

# なす角bの円弧の座標を計算
arc_b_x_vals = r * np.cos(t_b_vals)
arc_b_y_vals = r * np.sin(t_b_vals)
print(arc_b_x_vals[:5])
print(arc_b_y_vals[:5])
print(np.linalg.norm(np.vstack([arc_b_x_vals[:5], arc_b_y_vals[:5]]), axis=0))
[ 0.         -0.02579384 -0.05158768 -0.07738152 -0.10317536]
[0.         0.02379994 0.04759989 0.07139983 0.09519978]
[2.5        2.49916839 2.49667413 2.49251886 2.48670536]
[ 0.         -0.06447745 -0.128912   -0.19326079 -0.257481  ]
[2.5 2.5 2.5 2.5 2.5]
[2.5        2.49929199 2.49716835 2.49363029 2.48867981]
[0.         0.05949424 0.11895479 0.17834796 0.23764011]
[2.5 2.5 2.5 2.5 2.5]

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

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

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

 点  \tilde{\mathbf{a}}, \tilde{\mathbf{b}} それぞれと点  r \mathbf{e}_1 を結ぶ半径  r の円弧を描画する。

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

# 2Dベクトルとx軸を結ぶ円弧を作図
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_a_x_vals, arc_a_y_vals, 
        color='red', linewidth=2, linestyle='-.') # 円弧a
ax.plot(arc_b_x_vals, arc_b_y_vals, 
        color='blue', linewidth=2, linestyle='--') # 円弧b
ax.scatter(r, 0, 
           color='black', s=100, label='$r\ e_1 = (r, 0)$') # x軸線上の点
ax.scatter(*tilde_a, 
           color='red', s=100, 
           label='$\\tilde{a} = '+'({:.2f}, {:.2f})$'.format(*tilde_a)) # 点a
ax.scatter(*tilde_b, 
           color='blue', s=100, 
           label='$\\tilde{b} = '+'({:.2f}, {:.2f})$'.format(*tilde_b)) # 点b
ax.plot([0, r], [0, 0], 
        color='black', linewidth=2) # x軸線
ax.quiver(0, 0, *a, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルa
if norm_a < r:
    ax.plot([a[0], tilde_a[0]], [a[1], tilde_a[1]], 
            color='red', linewidth=2, linestyle=':') # ベクトルaの延長線
ax.quiver(0, 0, *b, 
          color='blue', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルb
if norm_b < r:
    ax.plot([b[0], tilde_b[0]], [b[1], tilde_b[1]], 
            color='blue', linewidth=2, linestyle=':') # ベクトルbの延長線
ax.text(d*r, 0, 
        s='$0$', size=15, ha='center', va='center') # ラジアンラベルe1
ax.text(*d*tilde_a, 
        s='$\\theta_a$', size=15, ha='center', va='center') # なす角aラベル
ax.text(*d*tilde_b, 
        s='$\\theta_b$', size=15, ha='center', va='center') # なす角bラベル
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)+', ' + 
             '\\theta_a = {:.2f} \pi'.format(theta_a/np.pi)+', ' + 
             '\\theta_b = {:.2f} \pi$'.format(theta_b/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軸を結ぶ円弧

 x軸線上の点  r \mathbf{e}_1 \theta_a, \theta_b 移動した点  \tilde{\mathbf{a}}, \tilde{\mathbf{b}} をそれぞれ結んだ円弧が得られた。

 なす角は劣角(  0 から  \pi のラジアン)なので、点  \tilde{\mathbf{a}}, \tilde{\mathbf{b}} の近い方の円弧で結ぶため、(点の位置を移動せずに)角度を変換する。

# 角度の差が優角なら負の角度を優角に変換
if abs(theta_a - theta_b) > np.pi:
    theta_a = theta_a if theta_a >= 0.0 else theta_a + 2.0*np.pi
    theta_b = theta_b if theta_b >= 0.0 else theta_b + 2.0*np.pi
print(theta_a)
print(theta_b)
3.7295952571373605
2.356194490192345

 角度の差の絶対値が優角(  \pi から  2 \pi のラジアン)  |\theta_a - \theta_b| \gt \pi であれば、負の角度(y座標が負の方の角度)に  2 \pi を加え優角に変換して、左回りの円弧から右周りの円弧にする。

 1つ前のコードで、円弧の座標を計算して、円弧を描画する(ラベルの処理は変更している)。

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

# ラベル用の文字列を作成
angle_a_label = '\\theta_a' if theta_a <= np.pi else '\\theta_a + 2 \pi'
angle_b_label = '\\theta_b' if theta_b <= np.pi else '\\theta_b + 2 \pi'

# 2Dベクトルとx軸を結ぶ円弧を作図
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_a_x_vals, arc_a_y_vals, 
        color='red', linewidth=2, linestyle='-.') # 円弧a
ax.plot(arc_b_x_vals, arc_b_y_vals, 
        color='blue', linewidth=2, linestyle='--') # 円弧b
ax.scatter(r, 0, 
           color='black', s=100, label='$r\ e_1 = (r, 0)$') # x軸線上の点
ax.scatter(*tilde_a, 
           color='red', s=100, 
           label='$\\tilde{a} = '+'({:.2f}, {:.2f})$'.format(*tilde_a)) # 点a
ax.scatter(*tilde_b, 
           color='blue', s=100, 
           label='$\\tilde{b} = '+'({:.2f}, {:.2f})$'.format(*tilde_b)) # 点b
ax.plot([0, r], [0, 0], 
        color='black', linewidth=2) # x軸線
ax.quiver(0, 0, *a, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルa
if norm_a < r:
    ax.plot([a[0], tilde_a[0]], [a[1], tilde_a[1]], 
            color='red', linewidth=2, linestyle=':') # ベクトルaの延長線
ax.quiver(0, 0, *b, 
          color='blue', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルb
if norm_b < r:
    ax.plot([b[0], tilde_b[0]], [b[1], tilde_b[1]], 
            color='blue', linewidth=2, linestyle=':') # ベクトルbの延長線
ax.text(d*r, 0, 
        s='$0$', size=15, ha='center', va='center') # ラジアンラベルe1
ax.text(*d*tilde_a, 
        s='$'+angle_a_label+'$', size=15, ha='center', va='center') # なす角aラベル
ax.text(*d*tilde_b, 
        s='$'+angle_b_label+'$', size=15, ha='center', va='center') # なす角bラベル
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)+', ' + 
             angle_a_label+'= {:.2f} \pi'.format(theta_a/np.pi)+', ' + 
             angle_b_label+'= {:.2f} \pi$'.format(theta_b/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軸を結ぶ円弧

  \theta_a, \theta_b に関するそれぞれの円弧を描画した。
 曲線が重ならない部分のみを描画するることで、 \mathbf{a}, \mathbf{b} のなす角に対応する円弧になる。

 点  \tilde{\mathbf{a}}, \tilde{\mathbf{b}} を結ぶ円弧の座標を計算する。

# 円弧用のラジアンを作成
u_vals = np.linspace(start=theta_a, stop=theta_b, 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))
[3.72959526 3.71572252 3.70184979 3.68797705 3.67410432]
[-2.08012574 -2.09916298 -2.11779624 -2.13602194 -2.15383655]
[-1.38675049 -1.35776094 -1.3285101  -1.29900358 -1.26924706]
[2.5 2.5 2.5 2.5 2.5]

  \theta_a から  \theta_b までのラジアン  u を用いて、先ほどの式で計算する。

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

# 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(*tilde_a, 
           color='red', s=100, 
           label='$\\tilde{a} = '+'({:.2f}, {:.2f})$'.format(*tilde_a)) # 点a
ax.scatter(*tilde_b, 
           color='blue', s=100, 
           label='$\\tilde{b} = '+'({:.2f}, {:.2f})$'.format(*tilde_b)) # 点b
ax.quiver(0, 0, *a, 
          color='red', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルa
if norm_a < r:
    ax.plot([a[0], tilde_a[0]], [a[1], tilde_a[1]], 
            color='red', linewidth=2, linestyle=':') # ベクトルaの延長線
ax.quiver(0, 0, *b, 
          color='blue', units='dots', width=3, headwidth=5, 
          angles='xy', scale_units='xy', scale=1) # ベクトルb
if norm_b < r:
    ax.plot([b[0], tilde_b[0]], [b[1], tilde_b[1]], 
            color='blue', linewidth=2, linestyle=':') # ベクトルbの延長線
ax.text(*d*tilde_a, 
        s='$'+angle_a_label+'$', size=15, ha='center', va='center') # なす角aラベル
ax.text(*d*tilde_b, 
        s='$'+angle_b_label+'$', size=15, ha='center', va='center') # なす角bラベル
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)+', ' + 
             angle_a_label+'= {:.2f} \pi'.format(theta_a/np.pi)+', ' + 
             angle_b_label+'= {:.2f} \pi'.format(theta_b/np.pi)+', ' + 
             '\\theta = {:.2f} \pi$'.format(abs(theta_a - theta_b)/np.pi), loc='left')
fig.suptitle('$\\theta = \\arccos(\\frac{a \cdot b}{\|a\| \|b\|})$', fontsize=20)
ax.grid()
ax.legend()
ax.set_aspect('equal')
plt.show()

円周上の2点を結ぶ円弧

 サイン関数とコサイン関数は  2 \pi で周期する  \sin \theta = \sin (\theta + 2 n \pi) \cos \theta = \cos (\theta + 2 n \pi) ので、円周上の点の座標は  (r \cos \theta, r \sin \theta) = (r \cos (\theta + 2 \pi), r \sin (\theta + 2 \pi)) である。

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

 ちなみに、theta_a, theta_b の差の絶対値は、2つのベクトルのなす角と一致する。

# ベクトルa,bのなす角(ラジアン)を計算
cos_theta = np.dot(a, b) / np.linalg.norm(a) / np.linalg.norm(b)
theta = np.arccos(cos_theta)
print(theta) # 0からπの値
print(theta / np.pi) # 0から1の値
1.373400766945016
0.43716704181099886

 詳しくは「なす角の描画」を参照のこと。

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

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

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

# 変化するベクトルを作成
alpha, beta = 4.0, 1.5
rad_n = np.linspace(start=0.0, stop=2.0*np.pi, num=frame_num+1)[:frame_num]
a_n = np.array(
    [alpha * np.cos(2.0 * rad_n), alpha * np.sin(2.0 * rad_n)]
).T
b_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(alpha), abs(beta)])) + 1

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

# 作図処理を関数として定義
def update(i):
    
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目のベクトルを取得
    a = a_n[i]
    b = b_n[i]
    
    # ノルムを調整
    tilde_a = r * a / np.linalg.norm(a)
    tilde_b = r * b / np.linalg.norm(b)
    
    # ベクトルa・bとx軸のなす角(ラジアン)を計算
    sgn_a   = 1.0 if a[1] >= 0.0 else -1.0
    theta_a = sgn_a * np.arccos(a[0] / np.linalg.norm(a))
    sgn_b   = 1.0 if b[1] >= 0.0 else -1.0
    theta_b = sgn_b * np.arccos(b[0] / np.linalg.norm(b))
    
    # 角度の差が優角なら負の角度を優角に変換
    if abs(theta_a - theta_b) > np.pi:
        theta_a = theta_a if theta_a >= 0.0 else theta_a + 2.0*np.pi
        theta_b = theta_b if theta_b >= 0.0 else theta_b + 2.0*np.pi
    
    # なす角a・bの円弧の座標を計算
    u_a_vals = np.linspace(start=0.0, stop=theta_a, num=100)
    arc_a_x_vals = r * np.cos(u_a_vals)
    arc_a_y_vals = r * np.sin(u_a_vals)
    u_b_vals = np.linspace(start=0.0, stop=theta_b, num=100)
    arc_b_x_vals = r * np.cos(u_b_vals)
    arc_b_y_vals = r * np.sin(u_b_vals)
    
    # 円弧の座標を計算
    u_vals = np.linspace(start=theta_a, stop=theta_b, num=100)
    arc_x_vals = r * np.cos(u_vals)
    arc_y_vals = r * np.sin(u_vals)
    
    # ラベル用の文字列を作成
    angle_a_label = '\\theta_a' if theta_a <= np.pi else '\\theta_a + 2 \pi'
    angle_b_label = '\\theta_b' if theta_b <= np.pi else '\\theta_b + 2 \pi'
    
    # 2Dベクトルを結ぶ円弧を作図
    d = 1.2
    e = 0.05
    ax.plot(circle_x_vals, circle_y_vals, 
            color='black', linewidth=1.5, linestyle=':') # 円周
    ax.plot(arc_a_x_vals*(1.0+e), arc_a_y_vals*(1.0+e), 
            color='red', linewidth=3) # 円弧a
    ax.plot(arc_b_x_vals*(1.0-e), arc_b_y_vals*(1.0-e), 
            color='blue', linewidth=3) # 円弧b
    ax.plot(arc_x_vals, arc_y_vals, 
            color='black', linewidth=3) # 円弧
    ax.scatter(r, 0, 
               color='black', s=100, label='$r\ e_1 = (r, 0)$') # x軸線上の点
    ax.scatter(*tilde_a, 
               color='red', s=100, 
               label='$\\tilde{a} = '+'({: .2f}, {: .2f})$'.format(*tilde_a)) # 点a
    ax.scatter(*tilde_b, 
               color='blue', s=100, 
               label='$\\tilde{b} = '+'({: .2f}, {: .2f})$'.format(*tilde_b)) # 点b
    ax.plot([0, r], [0, 0], 
            color='black', linewidth=2) # x軸線
    ax.quiver(0, 0, *a, 
              color='red', units='dots', width=3, headwidth=5, 
              angles='xy', scale_units='xy', scale=1) # ベクトルa
    if np.linalg.norm(a) < r:
        ax.plot([a[0], tilde_a[0]], [a[1], tilde_a[1]], 
                color='red', linewidth=2, linestyle=':') # ベクトルaの延長線
    ax.quiver(0, 0, *b, 
              color='blue', units='dots', width=3, headwidth=5, 
              angles='xy', scale_units='xy', scale=1) # ベクトルb
    if np.linalg.norm(b) < r:
        ax.plot([b[0], tilde_b[0]], [b[1], tilde_b[1]], 
                color='blue', linewidth=2, linestyle=':') # ベクトルbの延長線
    ax.text(d*r, 0, 
            s='$0$', size=15, ha='center', va='center') # ラジアンラベルe1
    ax.text(*d*tilde_a, 
            s='$'+angle_a_label+'$', size=15, ha='center', va='center') # なす角aラベル
    ax.text(*d*tilde_b, 
            s='$'+angle_b_label+'$', size=15, ha='center', va='center') # なす角bラベル
    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('$r = {}'+str(r)+', ' + 
                 angle_a_label+'= {: .2f} \pi'.format(theta_a/np.pi)+', ' + 
                 angle_b_label+'= {: .2f} \pi'.format(theta_b/np.pi)+', ' + 
                 '\\theta = {: .2f} \pi$'.format(abs(theta_a - theta_b)/np.pi), 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_arc_ab.gif')

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

 ベクトル  \mathbf{a}, \mathbf{b} の位置関係によって円弧を描画する向きが変わるのが分かる(伝わってほしい)。

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

おわりに

 なんでかとっても難産でした。同時並行して書いてた記事がいくつかあったのでそっちを先に書き終えようと後回しにしたら、そのまま放置して3か月が経ってました。書きたいことをいくつか分割して、別記事や別章にしたらなんとか書けました。
 これで頭の隅にずっとあったモヤモヤが1つ晴れました。

 ところで未だに角マークや角度マークと呼んでるアレの正式名称が分かりません。マークじゃなくてシンボルかもしれない。本当は塗り潰した方がいいのかもしれないとも思い始めました。誰か教えてください。
 それともっと良い実装方法があれば教えてください。

 2023年8月4日は、エビ中こと私立恵比寿中学の結成14周年の日です。

 私はハマって1年経ったか足らずな新参ですが、ここまで続けてくれていたから出会えました!ありがとうございます。これからもよろしくお願いします。

【次の内容】

www.anarchive-beta.com