Julia cis()関数の代替手段:exp(im*x)など比較解説

2025-05-27

Juliaにおけるcis()関数は、複素数の世界で非常に便利な機能を果たします。簡単に言うと、角度(ラジアン)を与えると、対応する単位円上の複素数を返す関数です。

数学的には、オイラーの公式 eix=cos(x)+isin(x) に基づいています。cis(x) はこの右辺、つまり cos(x)+isin(x) を計算するのと同義です。

以下に詳しく説明します。

cis()の基本的な働き

  • 出力
    その角度に対応する、絶対値が1の複素数(単位円上の点)を返します。具体的には、a+bi の形式で、ここで a=cos(x)、b=sin(x) となります。
  • 入力
    1つの実数(ラジアン単位の角度)を受け取ります。

なぜcis()が必要なのか?

  1. 簡潔性
    cos(x) + im*sin(x) と書く代わりに cis(x) と書くことで、コードがより短く、読みやすくなります。
  2. パフォーマンス
    内部的には、cossinを個別に計算するよりも効率的に、両方の値を同時に計算するように最適化されている場合があります。これは、三角関数を計算する際に通常、sinとcosの両方が同じ計算パスで得られるためです。
  3. 意図の明確化
    複素平面上の回転やフェーザ表現など、特に角度に関連する複素数の操作を行う際に、「単位円上の点」という意図が明確になります。
julia> cis(0)
1.0 + 0.0im # 0ラジアンは実軸上の1に対応

julia> cis(pi/2)
6.123233995736766e-17 + 1.0im # pi/2ラジアンは虚軸上の1に対応(浮動小数点誤差のため非常に小さい実部)

julia> cis(pi)
-1.0 + 1.2246467991473532e-16im # piラジアンは実軸上の-1に対応

julia> cis(3pi/2)
-1.8369701987210297e-16 - 1.0im # 3pi/2ラジアンは虚軸上の-1に対応

julia> cis(pi/4)
0.7071067811865476 + 0.7071067811865476im # cos(pi/4) + i*sin(pi/4)


角度の単位の間違い (Degrees vs. Radians)

これが最もよくある間違いです。 cis()ラジアン単位の角度を受け取ります。もし度数法で角度を与えてしまうと、全く異なる結果が得られます。

  • トラブルシューティング

    • 常にラジアンで入力する
      もし度数法で角度を持っている場合は、deg2rad() 関数を使ってラジアンに変換してください。
      # 90度をラジアンに変換してcis()に渡す
      angle_deg = 90
      angle_rad = deg2rad(angle_deg) # または angle_deg * pi / 180
      result = cis(angle_rad)
      println(result) # 出力: 6.123233995736766e-17 + 1.0im (ほとんど 0 + 1im)
      
    • ラジアンの基本を理解する
      • 0 度 = 0 ラジアン
      • 90 度 = π/2 ラジアン
      • 180 度 = π ラジアン
      • 270 度 = 3π/2 ラジアン
      • 360 度 = 2π ラジアン
    • cis(90) と入力して 0.5403023058681398 + 0.8414709848078965im のような、全く異なる複素数が返ってくる。
    • cis(180)-1.0 + 0.0im にならない。

非常に小さな浮動小数点誤差

cis() の結果は浮動小数点数で計算されるため、理論的には正確な 0 や 1 となるはずの場所で、非常に小さな非ゼロ値(例: 6.123233995736766e-17)が現れることがあります。これはエラーではなく、浮動小数点演算の性質によるものです。

  • トラブルシューティング

    • これは正常な動作であると理解する
      ほとんどの場合、これらの小さな誤差は無視できます。
    • 厳密な比較を避ける
      浮動小数点数同士の比較で == を使うのは避けるべきです。代わりに、isapprox() 関数を使って、ある許容範囲内で値が近いかどうかをチェックします。
      result = cis(pi/2)
      if isapprox(result, 0.0 + 1.0im)
          println("結果はほぼ 0 + 1im です。")
      end
      
    • 必要であれば丸める
      結果を可視化するためなどで、完全に 0 にしたい場合は round()trunc() などの関数を使うこともできますが、一般的には推奨されません。
      # 厳密な0にしたい場合 (注意して使用)
      real_part = round(real(cis(pi/2)), digits=10) # 小数点以下10桁で丸める
      imag_part = round(imag(cis(pi/2)), digits=10)
      clean_result = complex(real_part, imag_part)
      println(clean_result) # 出力: 0.0 + 1.0im
      
  • エラーの兆候

    • cis(pi/2)0.0 + 1.0im ではなく、6.123233995736766e-17 + 1.0im のようになる。
    • cis(pi)-1.0 + 0.0im ではなく、-1.0 + 1.2246467991473532e-16im のようになる。

引数の型エラー

cis() は数値(Real型)を引数として期待します。非数値型を渡そうとするとエラーになります。

  • トラブルシューティング

    • 引数が数値であることを確認する
      # 良い例
      angle = 1.23
      cis(angle)
      
      # 悪い例 (文字列)
      # angle_str = "1.23"
      # cis(angle_str) # エラー
      
    • 変数に格納されている場合は、typeof() を使って型を確認できます。
      my_var = "hello"
      println(typeof(my_var)) # String
      
  • エラーの兆候

    • MethodError: no method matching cis(::String) のようなエラー。

cis() は任意のラジアン値で機能しますが、非常に大きな角度(例: 1e10)や非常に小さな角度(例: 1e-20)を使用すると、浮動小数点演算の精度限界に起因する問題が発生する可能性があります。

  • トラブルシューティング

    • 角度を 0 から 2π の範囲に正規化する
      ほとんどのアプリケーションでは、角度を 0≤θ<2π の範囲に正規化することで、浮動小数点精度を最大限に活用できます。
      large_angle = 500 * pi # 例えば
      normalized_angle = mod(large_angle, 2 * pi) # または mod2pi()
      result = cis(normalized_angle)
      println(result)
      
      mod2pi(x) は、x を 0≤y<2π の範囲にマッピングします。
  • エラーの兆候

    • 期待される周期性(2π ごとに同じ値に戻る)が失われる。
    • 結果が不正確になる。


基本的な使用例:単位円上の点を取得する

最も基本的な使い方です。様々な角度で単位円上の複素数がどのように変化するかを確認できます。

# 0ラジアン (0度)
z0 = cis(0)
println("cis(0)         = $z0") # 出力: 1.0 + 0.0im (実軸の正の方向)

# pi/2ラジアン (90度)
z_pi_2 = cis(pi/2)
println("cis(pi/2)      = $z_pi_2") # 出力: 6.123233995736766e-17 + 1.0im (虚軸の正の方向、浮動小数点誤差あり)

# piラジアン (180度)
z_pi = cis(pi)
println("cis(pi)        = $z_pi") # 出力: -1.0 + 1.2246467991473532e-16im (実軸の負の方向)

# 3pi/2ラジアン (270度)
z_3pi_2 = cis(3pi/2)
println("cis(3pi/2)     = $z_3pi_2") # 出力: -1.8369701987210297e-16 - 1.0im (虚軸の負の方向)

# 2piラジアン (360度)
z_2pi = cis(2*pi)
println("cis(2*pi)      = $z_2pi") # 出力: 1.0 - 2.4492935982947064e-16im (実軸の正の方向、一回転)

# 任意の角度 (45度 = pi/4ラジアン)
z_pi_4 = cis(pi/4)
println("cis(pi/4)      = $z_pi_4") # 出力: 0.7071067811865476 + 0.7071067811865476im

度数法からラジアンへの変換

最も一般的なエラーは度数法を直接 cis() に渡すことです。deg2rad() 関数を使って安全に変換しましょう。

# 角度を度数法で定義
angle_degrees = 45.0

# 度数法をラジアンに変換
angle_radians = deg2rad(angle_degrees)
# または手動で変換: angle_radians = angle_degrees * (pi / 180.0)

# cis() で複素数を取得
z_45_deg = cis(angle_radians)
println("cis($angle_degrees degrees) = $z_45_deg") # 出力: cis(45.0 degrees) = 0.7071067811865476 + 0.7071067811865476im

# 90度の場合
angle_degrees_90 = 90.0
z_90_deg = cis(deg2rad(angle_degrees_90))
println("cis($angle_degrees_90 degrees) = $z_90_deg") # 出力: cis(90.0 degrees) = 6.123233995736766e-17 + 1.0im

オイラーの公式との比較

cis(x) が実際に cos(x)+isin(x) と等しいことを確認します。

x = pi / 3 # 60度

# cis() を使用
complex_cis = cis(x)

# cos() と sin() を使用
complex_manual = cos(x) + im * sin(x) # im は虚数単位 i を表す

println("cis($x)       = $complex_cis")
println("cos($x) + im*sin($x) = $complex_manual")

# 浮動小数点誤差を考慮して比較
println("isapprox(cis(x), cos(x) + im*sin(x)) = $(isapprox(complex_cis, complex_manual))")
# 出力: isapprox(cis(x), cos(x) + im*sin(x)) = true

複素数の極座標形式とデカルト座標形式の変換

cis() は、複素数を極座標形式 (reiθ) からデカルト座標形式 (x+iy) へ変換するのに非常に役立ちます。

# 極座標形式の複素数 (絶対値 r, 偏角 theta)
r = 5.0
theta = pi / 6 # 30度

# cis() を使ってデカルト座標に変換
# r * e^(i*theta) = r * (cos(theta) + i*sin(theta))
# = r * cis(theta)
z_polar = r * cis(theta)
println("極座標 ($r, $theta) の複素数 = $z_polar")
# 出力: 極座標 (5.0, 0.5235987755982988) の複素数 = 4.330127018922194 + 2.5im

# 確認: real() と imag() で実部と虚部を取得
println("実部: $(real(z_polar))")
println("虚部: $(imag(z_polar))")

# 逆変換 (デカルトから極座標)
# abs() で絶対値 (r) を取得
# angle() で偏角 (theta) を取得
absolute_value = abs(z_polar)
argument = angle(z_polar)
println("変換後の絶対値: $absolute_value")
println("変換後の偏角: $argument")

回転(フェーザ表現)

複素数を掛けることは、複素平面上での回転を意味します。cis() を使って特定の角度だけ回転させる新しい複素数を生成できます。

# 元の複素数 (例: 実軸上の点)
z_original = 2.0 + 0.0im
println("元の複素数: $z_original")

# 90度 (pi/2ラジアン) 回転させる複素数
rotator_90 = cis(pi/2)
println("90度回転させる因子: $rotator_90")

# 回転後の複素数
z_rotated_90 = z_original * rotator_90
println("90度回転後の複素数: $z_rotated_90") # 出力: 1.2246467991473532e-16 + 2.0im (ほとんど 0 + 2im)

# -45度 (-pi/4ラジアン) 回転させる
rotator_neg_45 = cis(-pi/4)
println("-45度回転させる因子: $rotator_neg_45")

z_rotated_neg_45 = z_original * rotator_neg_45
println("-45度回転後の複素数: $z_rotated_neg_45") # 出力: 1.414213562373095 + -1.414213562373095im

ド・モアブルの定理 (reiθ)n=rneinθ を利用して、複素数のn乗根を計算できます。

# 1の3乗根を求める
# 1 = 1 * e^(i*0) = 1 * cis(0)
# 1のn乗根は e^(i * (2*pi*k / n)) で与えられる (k = 0, 1, ..., n-1)

n = 3 # 3乗根

println("1の3乗根:")
for k = 0:(n-1)
    root = cis(2 * pi * k / n)
    println("k=$k: $root")
end
# 出力:
# k=0: 1.0 - 2.4492935982947064e-16im (1)
# k=1: -0.4999999999999998 + 0.8660254037844386im (e^(i*2pi/3))
# k=2: -0.5000000000000002 - 0.8660254037844386im (e^(i*4pi/3))


cos(x) + im * sin(x) を直接記述する

これは cis() が内部で行っていることと本質的に同じです。最も直接的な代替方法であり、cis() の機能が正確に何であるかを理解するのに役立ちます。

x = pi/3 # 60度

# cis() を使用
z_cis = cis(x)
println("cis(x) = $z_cis")

# cos() と sin() を直接使用
z_manual = cos(x) + im * sin(x)
println("cos(x) + im * sin(x) = $z_manual")

println("等しいか? $(z_cis ≈ z_manual)") # 浮動小数点誤差を考慮して比較

利点

  • Julia の基本的な数学関数 (cos, sin) と複素数型 (im) の知識があれば理解できる。
  • cis() が何を計算しているか明確。

欠点

  • 場合によっては、cis() の方が内部的に最適化されている可能性がある(例えば、sincos() のように両方の値を同時に計算できる場合)。
  • cis() よりも記述が長い。

exp(im * x) を使用する(オイラーの公式)

オイラーの公式 eix=cos(x)+isin(x) に基づくと、cis(x)exp(im * x) と全く同じ結果を返します。数学的な表現に忠実であり、複素指数関数としての意味合いを強調したい場合に特に有効です。

x = pi/4 # 45度

# cis() を使用
z_cis = cis(x)
println("cis(x) = $z_cis")

# exp(im * x) を使用
z_exp = exp(im * x)
println("exp(im * x) = $z_exp")

println("等しいか? $(z_cis ≈ z_exp)") # 浮動小数点誤差を考慮して比較

利点

  • 複素指数関数として、より一般的な複素数の操作(例: ea+ib=eaeib)と一貫性がある。
  • オイラーの公式に直接対応しており、数学的背景が明確。

欠点

  • わずかなパフォーマンスの違いがあるかもしれないが、現代のJITコンパイラでは通常、ほとんど差がない。
  • cis() が意図している「単位円上の点」という特定の用途に対しては、exp(im * x) の方が少し冗長に感じられるかもしれない。

極座標表現から変換する

Julia の Complex 型は、デカルト座標 (x+iy) を直接扱いますが、内部的には極座標 (reiθ) とも密接に関連しています。cis(x) は、r=1 の場合の極座標からデカルト座標への変換と考えることができます。

複素数をその絶対値 (magnitude) と偏角 (angle) から構成する場合、以下の関数を使うことができます。

# 目的: 絶対値 r、偏角 theta の複素数を生成したい
r = 2.5
theta = pi/6 # 30度

# cis() を使用して、単位円上の点を生成し、r を掛ける
z_polar_cis = r * cis(theta)
println("r * cis(theta) = $z_polar_cis")

# Complex() コンストラクタに実部と虚部を直接渡す
# ここでは、実部 r*cos(theta) と虚部 r*sin(theta) を計算
z_polar_manual = Complex(r * cos(theta), r * sin(theta))
println("Complex(r*cos(theta), r*sin(theta)) = $z_polar_manual")

println("等しいか? $(z_polar_cis ≈ z_polar_manual)")

利点

  • cis() が提供する特定の機能を理解するのに役立つ。
  • 複素数の根本的な構造を理解するのに役立つ。
  • cis() の方が、r=1 の場合の「単位円上の点」を生成する意図がより明確。
  • cis() の内部動作を理解するために
    cos(x) + im * sin(x)

    • デバッグや学習目的には良いですが、日常的なコードでは cis() を使うべきです。
  • 数学的な背景を強調したい場合
    exp(im * x)

    • 特にオイラーの公式の文脈で複素指数関数を扱っていることが明確になります。
  • 最も推奨される方法
    cis(x)

    • これは、その意図(単位円上の複素数)を最も明確に表現し、コードを簡潔にします。
    • パフォーマンスは通常、他の方法と同等か、わずかに優れています。