M.Hiroi's Home Page

JavaScript Programming

JavaScript Junk Scripts


Copyright (C) 2025 Makoto Hiroi
All rights reserved.

複素数

近年、多くのプログラミング言語で「複素数 (complex number)」がサポートされるようになりました。たとえば、C言語では 1999 年に発行された規格 C99 で複素数型が導入されました。Go 言語や Python でも複素数型をサポートしていますし、複素数用の標準ライブラリを用意している言語 (C++, Ruby, Haskell など) も多くあります。

他の言語では、FORTRAN や Common Lisp が昔から複素数型をサポートしています。Common Lisp の場合、基本的な数学関数でも複素数を適用できるのであれば、引数に複素数を渡して計算することができます。JavaScript は標準で複素数をサポートしていませんが、複素数や行列などを扱うライブラリ (たとえば math.js) が公開されています。今回は JavaScript のお勉強ということで、あえて複素数の計算を行うプログラムを作ってみましょう。

●JavaScript の数

JavaScript の数は実数 (Number) と多倍長整数 (BigInt) の 2 種類があります。BigInt は ES2020 から導入されました。Number と BigInt を混在させて計算することはできません。エラーになります。

実数は浮動小数点数 (floating point number) として表現されます。浮動小数点数には IEEE 754 という標準仕様があり、近代的なプログラミング言語のほとんどは、IEEE 754 に準拠した浮動小数点数をサポートしています。浮動小数点数はすべての小数を正確に表現することはできません。このため、実数は近似的 (不正確) な値になります。

IEEE 754 で表現可能な正の最大値と最小値は、Number の静的プロパティに格納されています。

> Number.MAX_VALUE
1.7976931348623157e+308

> Number.MIN_VALUE
5e-324

また、IEEE 754 で正確に表される整数の範囲は、-(2 ** 53) + 1 = -9007199254740991 から 2 ** 53 - 1 = 9007199254740991 までです。これらの値は Number の静的プロパティに格納されています。

> Number.MAX_SAFE_INTEGER
9007199254740991

> Number.MIN_SAFE_INTEGER
-9007199254740991

静的メソッド Number.isSafeInteger() は、渡された引数が上記の範囲内であれば真を返します。

> Number.isSafeInteger(2 ** 53 - 1)
true
> Number.isSafeInteger(2 ** 53)
false

> Number.isSafeInteger(-(2 ** 53))
false
> Number.isSafeInteger(-(2 ** 53) + 1)
true

IEEE 754 には通常の数値以外にも、負のゼロ (-0.0)、正負の無限大 (∞, -∞)、NaN (Not a Number, 非数) といった値が定義されています。これらの値は JavaScript でも取り扱うことができます。負のゼロは -0、正負の無限大は Infinity と -Infinity、NaN は NaN として定義されています。

●無限大

一般に、無限大は値のオーバーフロー、ゼロ除算 (数値 / 0.0)、数学関数の計算結果 (たとえば log(0.0)) などで発生します。なお、浮動小数点数のゼロ除算でエラー (例外) を送出する処理系 (たとえば Python など) もあります。

簡単な実行例を示します。

> Infinity
Infinity
> -Infinity
-Infinity
> 1e308
1e+308
> 1e309
Infinity
> -1e308
-1e+308
> -1e308 * 2
-Infinity
> 1.0 / 0.0
Infinity
> -1.0 / 0.0
-Infinity

Number.isFinite() は引数が有限数ならば真を返します。

> Number.isFinite(1e308)
true
> Number.isFinite(Infinity)
false
> Number.isFinite(-Infinity)
false

無限大は他の数値と比較したり演算することもできますが、結果が NaN になることもあります。

> Infinity == Infinity
true
> Infinity == -Infinity
false
> -Infinity < Infinity
true
> Infinity < -Infinity
false
> Infinity < 0
false
> Infinity > 0
true

> Infinity + 10
Infinity
> Infinity - 10
Infinity
> Infinity * 10
Infinity
> Infinity / 10
Infinity

> Infinity + Infinity
Infinity
> Infinity * Infinity
Infinity

> Infinity / Infinity
NaN
> Infinity - Infinity
NaN
> Infinity + -Infinity
NaN
> Infinity * 0.0
NaN

●負のゼロ

負のゼロ (-0.0) は、計算結果が負の極めて小さな値でアンダーフローになったとき発生します。また、正の値を負の無限大で除算する、負の値を正の無限大で除算する、負の値と 0.0 を乗算しても -0.0 が得られます。

> -1e-323
-1e-323
> -1e-324
-0

> -1e-323 / 2
-5e-324
> -1e-323 / 4
-0

> 1.0 / -Infinity
-0
> -1.0 / Infinity
-0

> -1.0 * 0.0
-0

標準 (IEEE 754) では、演算子 (JavaScript では ==) による 0.0 と -0.0 の比較は等しいと判定されます。JavaScript には -0.0 を判定する関数は用意されていませんが、1.0 / 0.0 = Infnity, 1.0 / -0.0 = -Infnity になることを利用すると、0.0 と -0.0 を区別することができます。

> 0.0 == -0.0
true
> -0.0 == 0.0
true
> 0.0 != -0.0
false

> function isNegativeZero(n) { return n == 0.0 && 1.0 / n == -Infinity; }
undefined
> isNegativeZero(1.0)
false
> isNegativeZero(0.0)
false
> isNegativeZero(-0.0)
true
> isNegativeZero(-1.0)
false

なお、-0.0 は数学関数や複素数の演算処理などで使われます。

> Math.sqrt(0.0)
0
> Math.sqrt(-0.0)
-0

> Math.atan2(0.0, -1.0)
3.141592653589793
> Math.atan2(-0.0, -1.0)
-3.141592653589793

●非数

NaN は数ではないことを表す特別な値 (非数) です。一般的には 0.0 / 0.0 といった不正な演算を行うと、その結果は NaN になります。

> NaN
NaN
> NaN / NaN
NaN
> Infinity / Infinity
NaN
> 0.0 / 0.0
NaN

JavaScript の場合、NaN は静的メソッド Number.isNaN() で判別することができます。Number.isFinite() は引数が無限大でも NaN でもなければ真を返します。標準 (IEEE 754) では NaN == NaN を偽と判定します。

> Number.isNaN(NaN)
true
> Number.isNaN(Infinity)
false
> Number.isNaN(1.234)
false

> Number.isFinite(NaN)
false
> Number.isFinite(Infinity)
false
> Number.isFinite(1.234)
true

> NaN == NaN
false
> NaN != NaN
true

●複素数の定義

数学では複素数 z を \(x + iy\) と表記します。x を実部、y を虚部、i を虚数単位といいます。虚数単位は 2 乗すると -1 になる数です。実部と虚部の 2 つの数値を格納するデータ構造を用意すれば、プログラミング言語で複素数を表すことができます。

リスト : 複素数型

class Complex {
    constructor(x = 0, y = 0) {
        this._real = x;
        this._imag = y;
    }
    get real() { return this._real; }
    get imag() { return this._imag; }

    // 等値
    equals(other) {
        return this.real == other.real && this.imag == other.imag;
    }
    // 文字列
    toString() { return `C(${this.real}, ${this.imag})`; }
    // ... 略 ...
}

クラス名は Complex としました。実部をインスタンス変数 _real に、虚部を _imag にセットします。実部は real で、虚部は imag で取得します。複素数は new Complex(x, y) で生成します。引数 x が実部で、y が虚部です。メソッド toString() は複素数 \(x + iy\) を文字列 C(x, y) に変換します。console.log() の第 1 引数に '%s' を指定すれば、画面に表示することができます。

複素数 \(z = x + iy\) の虚部の符号を反転した数 \(x - iy\) を複素共役といいます。複素数を極形式 \(z = |z|(\cos \theta + i \sin \theta)\) で表した場合、\(|z|\) を絶対値、\(\theta\) を偏角といいます。絶対値 \(|z|\) の定義は \(\sqrt{x^2 + y^2}\) です。偏角は数学関数 atan2(y, x) で求めることができます。これをプログラムすると次のようになります。

リスト : 複素共役, 絶対値, 偏角

    // 複素共益
    conj() { return new Complex(this.real, -this.imag); }
    // 偏角
    arg() { return Math.atan2(this.imag, this.real); }
    // 絶対値
    abs() { return Math.hypot(this.real, this.imag); }

複素共役を求めるメソッド conj() と偏角を求めるメソッド arg() は簡単ですね。atan2(y, x) は直交座標においてベクトル (x, y) と x 軸との角度を求める関数です。角度 \(\theta\) の範囲は \(-\pi \leqq \theta \leqq \pi\) になります。簡単な例を示しましょう。

> Math.atan2(0, 1)
0
> Math.atan2(1, 1)
0.7853981633974483
> Math.atan2(1, 0)
1.5707963267948966
> Math.atan2(0, -1)
3.141592653589793
> Math.atan2(-1, 1)
-0.7853981633974483
> Math.atan2(-1, 0)
-1.5707963267948966
> Math.atan2(-1, -1)
-2.356194490192345
> Math.atan2(-0.1, -1)
-3.0419240010986313
> Math.atan2(-0.01, -1)
-3.131592986903128
> Math.atan2(-0.001, -1)
-3.1405926539231266
> Math.atan2(-0.0, -1)
-3.141592653589793

\(y \geqq 0\) の場合、atan2 の返り値は \(0 \leqq \theta \leqq \pi\) になり、\(y \lt 0\) の場合は \(-\pi \leqq \theta \lt 0\) になります。

絶対値を求めるメソッド abs() は、定義をそのままプログラムすると二乗の計算でオーバーフローすることがあります。たとえば、C(1e300, 1e300) の絶対値を求めてみましょう。このとき、1e300 の二乗でオーバーフローします。

> 1e300 * 1e300
Infinity

参考文献『C言語による最新アルゴリズム事典』によると、次のように場合分けすることで、\(x^2\) や \(y^2\) で生じ得る上位桁あふれを回避することができるそうです。

\( \sqrt{x^2 + y^2} = \begin{cases} |x| \sqrt{1 + \left(\dfrac{y}{x}\right)^2}\ \quad & \mathrm{if} \ |x| \geq |y| \\ |y| \sqrt{1 + \left(\dfrac{x}{y}\right)^2}\ \quad & \mathrm{other} \end{cases} \)

幸いなことに、JavaScript の関数 Math.hypot() はオーバーフローの対策が施されているので、今回はそれを使うことにしましょう。

それでは実際に試してみましょう。ソースファイル complex.js はモジュールとして定義されているので、REPL で試すときはダイナミックインポートで読み込んでください。

> const { default: Complex } = await import("./complex.js")
undefined
> var a = new Complex(1, 2)
undefined
> var b = new Complex(1, 2)
undefined
> a.equals(b)
true
> a == b
false
> a == a
true

> console.log('%s', new Complex(1, 1))
C(1, 1)
undefined
> console.log('%s', (new Complex(1, 1)).conj())
C(1, -1)
undefined
> console.log('%s', (new Complex(1, 1)).conj().conj())
C(1, 1)
undefined

> (new Complex(1, 1)).abs()
1.4142135623730951
> (new Complex(1, -1)).abs()
1.4142135623730951
> (new Complex(1e300, 1e300)).abs()
1.4142135623730952e+300
> (new Complex(1e301, 1e300)).abs()
1.004987562112089e+301
> (new Complex(1e300, 1e301)).abs()
1.004987562112089e+301

> (new Complex(1, 0)).arg()
0
> (new Complex(1, 1)).arg()
0.7853981633974483
> (new Complex(0, 1)).arg()
1.5707963267948966
> (new Complex(-1, 1)).arg()
2.356194490192345
> (new Complex(-1, 0)).arg()
3.141592653589793
> (new Complex(1, -1)).arg()
-0.7853981633974483
> (new Complex(0, -1)).arg()
-1.5707963267948966
> (new Complex(-1, -1)).arg()
-2.356194490192345

●複素数の四則演算

複素数の四則演算は次のようになります。

\( \begin{array}{l} (a + bi) + (c + di) = (a + c) + (b + d)i \\ (a + bi) - (c + di) = (a - c) + (b - d)i \\ (a + bi) \times (c + di) = (ac - bd) + (bc + ad)i \\ (a + bi) \div (c + di) = \dfrac{ac + bd + (bc - ad)i}{c^2 + d^2} \end{array} \)

除算の場合、絶対値の計算と同様にオーバーフローの対策が必要になります。今回は『C言語による最新アルゴリズム事典』のプログラムを JavaScript に移植しました。プログラムは次のようになります。

リスト : 複素数の四則演算

    // 加算
    add(other) {
        return new Complex(this.real + other.real, this.imag + other.imag);
    }
    // 減算
    sub(other) {
        return new Complex(this.real - other.real, this.imag - other.imag);
    }
    // 乗算
    mul(other) {
        let {_real: xr, _imag: xi} = this;
        let {_real: yr, _imag: yi} = other;
        return new Complex(xr * yr - xi * yi, xr * yi + xi * yr);
    }
    // 除算
    div(other) {
        let {_real: xr, _imag: xi} = this;
        let {_real: yr, _imag: yi} = other;
        if (Math.abs(yr) >= Math.abs(yi)) {
            let u = yi / yr;
            let v = yr + yi * u;
            return new Complex((xr + xi * u) / v, (xi - xr * u) / v);
        } else {
            let u = yr / yi;
            let v = yr * u + yi;
            return new Complex((xr * u + xi) / v, (xi * u - xr) / v);
        }
    }

それでは実際に試してみましょう。

> var a = new Complex(1, 2)
undefined
> var b = new Complex(3, 4)
undefined
> console.log('%s', a.add(b))
C(4, 6)
undefined
> console.log('%s', a.sub(b))
C(-2, -2)
undefined
> console.log('%s', a.mul(b))
C(-5, 10)
undefined
> console.log('%s', a.div(b))
C(0.44, 0.08)
undefined
> var one = new Complex(1, 0)
undefined
> console.log('%s', one.div(new Complex(1e300, 1e300)))
C(5e-301, -5e-301)
undefined
> console.log('%s', one.div(new Complex(1e301, 1e300)))
C(9.9009900990099e-302, -9.9009900990099e-303)
undefined
> console.log('%s', one.div(new Complex(1e300, 1e301)))
C(9.9009900990099e-303, -9.9009900990099e-302)
undefined

●複素数の指数関数と対数関数

\( \begin{array}{l} e^{i\theta} = \cos \theta + i \sin \theta \quad (Euler's formula) \\ \begin{eqnarray} e^{x+iy} &=& e^x e^{iy} \\ &=& e^x (\cos y + i \sin y) \end{eqnarray} \end{array} \)

複素数の対数関数は複素数 z を絶対値 \(|z|\) と偏角 \(\theta\) を使って導くことができます。

\( \begin{array}{l} x + iy = |z| e^{i\theta} \\ \begin{eqnarray} \log_e (x + iy) &=& log_e |z| e^{i\theta} \\ &=& log_e |z| + log_e e^{i\theta} \\ &=& log_e |z| + i\theta, \quad (-\pi \leq \theta \leq \pi) \end{eqnarray} \end{array} \)

複素数 x, y のべき乗 \(x^y\) は次式で求めることができます。

\(x^y = e^{y\log x}\)

これをそのままプログラムすると次のようになります。

リスト : 指数関数、対数関数、べき乗

    // 指数関数
    static exp(x) {
        let e = Math.exp(x.real);
        return new Complex(e * Math.cos(x.imag), e * Math.sin(x.imag));
    }
    // 対数関数
    static log(x) {
        return new Complex(Math.log(x.abs()), x.arg());
    }
    // べき乗
    static pow(x, y) {
        return Complex.exp(y.mul(Complex.log(x)));
    }

簡単な例を示しましょう。

> let pi = Math.PI
undefined
> pi
3.141592653589793

> console.log('%s', Complex.exp(new Complex(0, pi/4)))
C(0.7071067811865476, 0.7071067811865475)
undefined
> console.log('%s', Complex.exp(new Complex(0, pi/2)))
C(6.123233995736766e-17, 1)
undefined
> console.log('%s', Complex.exp(new Complex(0, pi)))
C(-1, 1.2246467991473532e-16)
undefined
> console.log('%s', Complex.exp(new Complex(1, 1)))
C(1.4686939399158851, 2.2873552871788423)
undefined

> console.log('%s', Complex.log(new Complex(1, 0)))
C(0, 0)
undefined
> console.log('%s', Complex.log(new Complex(0, 1)))
C(0, 1.5707963267948966)
undefined
> console.log('%s', Complex.log(new Complex(1, -1)))
C(0.3465735902799727, -0.7853981633974483)
undefined
> console.log('%s', Complex.log(new Complex(1e300, 1e300)))
C(691.1221014884936, 0.7853981633974483)

> var a = new Complex(1, 1)
undefined
> console.log('%s', Complex.pow(a, new Complex(0, 0)))
C(1, 0)
undefined
> console.log('%s', Complex.pow(a, new Complex(1, 0)))
C(1.0000000000000002, 1)
undefined
> console.log('%s', Complex.pow(a, new Complex(2, 0)))
C(1.2246467991473532e-16, 2)
undefined
> console.log('%s', Complex.pow(a, new Complex(3, 0)))
C(-2.0000000000000004, 2.0000000000000004)

> console.log('%s', Complex.pow(a, a))
C(0.2739572538301211, 0.5837007587586147)
undefined
> console.log('%s', Complex.pow(new Complex(1, 2), new Complex(3, 4)))
C(0.12900959407446697, 0.03392409290517014)
undefined

関数 \(\log z \ (z = x + iy)\) は負の実軸 (\(-\infty \lt x \lt 0\)) において、x + i 0.0 と x - i 0.0 では値が異なります。

> console.log('%s', Complex.log(new Complex(-1, 0)))
C(0, 3.141592653589793)
undefined
> console.log('%s', Complex.log(new Complex(-1, -0)))
C(0, -3.141592653589793)
undefined
> console.log('%s', Complex.log(new Complex(-1e300, 0)))
C(690.7755278982137, 3.141592653589793)
undefined
> console.log('%s', Complex.log(new Complex(-1e300, -0)))
C(690.7755278982137, -3.141592653589793)
undefined

このように、関数 \(\log z\) は負の実軸上で 2 つの値を持ちます。数学では値を一つ返す関数を「一価関数」、複数の値を返す関数を「多価関数」といいます。ここで、定義域を制限することで多価関数を一価関数にみなすことを考えます。関数 \(\log z\) の場合、負の実軸を定義域から取り除けば、\(\log z\) を一価関数とみなすことができるわけです。

参考 URL 『逆双曲線関数と逆三角関数の branch cut | 雑記帳』によると、この取り除いた領域を branch cut と呼ぶそうです。プログラミングでは branch cut を定義域から取り除くのではなく、その領域では不連続な関数とするそうです。

Python のモジュール cmath のドキュメントには、\(\log\) は「分枝切断を一つもち、0 から負の実数軸に沿って \(-\infty\) へと延びており」と記述されています。Python のマニュアルに合わせて、本稿でも branch cut を「分枝切断」と記述することにします。

プログラミング言語の場合、0.0 と -0.0 を区別する処理系であれば、Python のように 2 つの値を区別することができます。0.0 と -0.0 を区別しない処理系では、偏角 \(\theta\) の範囲を \(-\pi \lt \theta \leq \pi\) に制限することで、\(\log z\) の返り値を (\(-\pi\) を取り除いて) 一つにすることができます。

●複素数の三角関数

複素数の三角関数の定義は、オイラーの公式から導かれる式の \(\theta\) を複素数 \(z\) に変えたものになります。

\(\sin -\theta = -\sin \theta, \ \cos -\theta = \cos \theta \) より
\( \begin{eqnarray} e^{i(-\theta)} &=& \cos -\theta + i \sin -\theta \\ &=& \cos \theta - i \sin \theta \end{eqnarray} \)

\( \begin{array}{l} e^{i\theta} + e^{-i\theta} = 2 \cos \theta \\ \cos \theta = \dfrac{e^{i\theta} + e^{-i\theta}}{2} \\ e^{i\theta} - e^{-i\theta} = 2i \sin \theta \\ \sin \theta = \dfrac{e^{i\theta} - e^{-i\theta}}{2i} \end{array} \)

\(\theta\) を複素数 z に置き換えた式が三角関数の定義になる

\( \sin z = \dfrac{e^{iz} - e^{-iz}}{2i}, \quad \cos z = \dfrac{e^{iz} + e^{-iz}}{2} \)

\(\sin z, \cos z\) に純虚数 \(ix\) を与えると双曲線関数 (\(\sinh x, \cosh x\)) になります。

双曲線関数の定義
\(\begin{eqnarray} \sinh x = \dfrac{e^x - e^{-x}}{2} \\ \cosh x = \dfrac{e^x + e^{-x}}{2} \end{eqnarray}\)
\(\begin{eqnarray} \sin ix &=& \dfrac{e^{iix} - e^{-iix}}{2i} \\ &=& \dfrac{e^{-x} - e^x}{2i} \times \dfrac{-i}{-i} \\ &=& i \dfrac{e^x - e^{-x}}{2} \\ &=& i \sinh x \\ \cos ix &=& \dfrac{e^{iix} + e^{-iix}}{2} \\ &=& \dfrac{e^{-x} + e^x}{2} \\ &=& \cosh x \end{eqnarray}\)

これに三角関数の加法定理 [*1] を使うと次の式が導かれます。

\(\begin{eqnarray} \sin (x + iy) &=& \sin x \cos iy + \cos x \sin iy \\ &=& \sin x \cosh y + i \cos x \sinh y \\ \cos (x + iy) &=& \cos x \cos iy - \sin x \sin iy \\ &=& \cos x \cosh y - i \sin x \sinh y \\ \tan (x + iy) &=& \dfrac{\sin 2x + \sin 2iy}{\cos 2x + \cos 2iy} \\ &=& \dfrac{\sin 2x + i \sinh 2y}{\cos 2x + \cosh 2y} \end{eqnarray}\)

これをそのままプログラムすると、次のようになります。

リスト : 三角関数

    static sin(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.sin(x) * Math.cosh(y), Math.cos(x) * Math.sinh(y));
    }

    static cos(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.cos(x) * Math.cosh(y), - Math.sin(x) * Math.sinh(y));
    }

    static tan(z) {
        let x = z.real;
        let y = z.imag;
        let d = Math.cos(2.0 * x) + Math.cosh(2.0 * y);
        return new Complex(Math.sin(2.0 * x) / d, Math.sinh(2.0 * y) / d);
    }

簡単な実行例を示します。

> var a = [ new Complex(0, 1), new Complex(0, -1), new Complex(1, 1), new Complex(1, -1)]
undefined
> for (x of a) console.log('%s', Complex.sin(x))
C(0, 1.1752011936438014)
C(0, -1.1752011936438014)
C(1.2984575814159773, 0.6349639147847361)
C(1.2984575814159773, -0.6349639147847361)
undefined
> for (x of a) console.log(Complex.sin(x).abs())
1.1752011936438014
1.1752011936438014
1.4453965766582497
1.4453965766582497
undefined

> for (x of a) console.log('%s', Complex.cos(x))
C(1.5430806348152437, 0)
C(1.5430806348152437, 0)
C(0.8337300251311491, -0.9888977057628651)
C(0.8337300251311491, 0.9888977057628651)
undefined
> for (x of a) console.log(Complex.cos(x).abs())
1.5430806348152437
1.5430806348152437
1.2934544550420957
1.2934544550420957
undefined

> for (x of a) console.log('%s', Complex.tan(x))
C(0, 0.761594155955765)
C(0, -0.761594155955765)
C(0.27175258531951174, 1.0839233273386948)
C(0.27175258531951174, -1.0839233273386948)
undefined
> for (x of a) console.log(Complex.tan(x).abs())
0.761594155955765
0.761594155955765
1.1174700207060706
1.1174700207060706
undefined
-- note --------
[*1] 三角関数の公式は引数が複素数でも成り立ちます。ただし、\(|\sin x| \leq 1, |\cos x| \leq 1\) という関係式は、x が実数だと成立しますが複素数では成立しません。

●複素数の双曲線関数

複素数の双曲線関数の定義は、実数の定義で引数 x を複素数 z に変えたものになります。

双曲線関数の定義 (z は複素数)
\(\begin{eqnarray} \sinh z = \dfrac{e^{z} - e^{-z}}{2} \\ \cosh z = \dfrac{e^{z} + e^{-z}}{2} \end{eqnarray}\)

sinh z, cosh z に純虚数 ix を与えると三角関数 (sin x, cos x) になります。

\(\begin{eqnarray} \sinh ix &=& \dfrac{e^{ix} - e^{-ix}}{2} \\ &=& \dfrac{e^{ix} - e^{-ix}}{2} \times \dfrac{i}{i} \\ &=& i \dfrac{e^{ix} - e^{-ix}}{2i} \\ &=& i \sin x \\ \cosh ix &=& \dfrac{e^{ix} + e^{ix}}{2} \\ &=& \cos x \end{eqnarray}\)

これに双曲線関数の加法定理を使うと、次の式が導かれます。

双曲線関数の加法定理
\(\begin{eqnarray} \sinh(x + y) &=& \sinh x \cosh y + \cosh x \sinh y \\ \cosh(x + y) &=& \cosh x \cosh y + \sinh x \sinh y \\ \tanh(x + y) &=& \dfrac{\sinh(x + y)}{\cosh(x + y)} \\ &=& \dfrac{\sinh 2x + \sinh 2y}{\cosh 2x + \cosh 2y} \end{eqnarray}\)
\(\begin{eqnarray} \sinh(x + iy) &=& \sinh x \cos y + i \cosh x \sin y \\ \cosh(x + iy) &=& \cosh x \cos y + i \sinh x \sin y \\ \tanh(x + iy) &=& \dfrac{\sinh(x + iy)}{\cosh(x + iy)} \\ &=& \dfrac{\sinh 2x + i \sin 2y}{\cosh 2x + \cos 2y} \end{eqnarray}\)

これをそのままプログラムすると、次のようになります。

リスト : 双曲線関数

   static sinh(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.sinh(x) * Math.cos(y), Math.cosh(x) * Math.sin(y));
    }

    static cosh(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.cosh(x) * Math.cos(y), Math.sinh(x) * Math.sin(y));
    }

    static tanh(z) {
        let x = z.real;
        let y = z.imag;
        let d = Math.cosh(2.0 * x) + Math.cos(2.0 * y);
        return new Complex(Math.sinh(2.0 * x) / d, Math.sin(2.0 * y) / d);
    }

簡単な使用例を示します。

> var a = [ new Complex(0, 1), new Complex(0, -1), new Complex(1, 1), new Complex(1, -1)]
undefined
> for (x of a) console.log('%s', Complex.sinh(x))
C(0, 0.8414709848078965)
C(0, -0.8414709848078965)
C(0.6349639147847361, 1.2984575814159773)
C(0.6349639147847361, -1.2984575814159773)
undefined
> for (x of a) console.log('%s', Complex.cosh(x))
C(0.5403023058681398, 0)
C(0.5403023058681398, 0)
C(0.8337300251311491, 0.9888977057628651)
C(0.8337300251311491, -0.9888977057628651)
undefined
> for (x of a) console.log('%s', Complex.tanh(x))
C(0, 1.557407724654902)
C(0, -1.557407724654902)
C(1.0839233273386948, 0.27175258531951174)
C(1.0839233273386948, -0.27175258531951174)
undefined

ところで、cosh(C(0, -1)) の虚部は -0 になります。

> Complex.cosh(new Complex(0, -1))
Complex { _real: 0.5403023058681398, _imag: -0 }

toString() は -0 を '0' に変換するため、次のように表示されます。

> Complex.cosh(new Complex(0, -1)).toString()
'C(0.5403023058681398, 0)'

計算結果は間違っていませんが、表示方法に問題があるようです。

●複素数の平方根

複素数 z の平方根は次の式で求めることができます。

\(z = x + iy, \ |z| = \sqrt{x^2 + y^2}, \ \theta = \arg z \ (-\pi \leq \theta \leq \pi)\) とすると

\( \sqrt{x + iy} = \begin{cases} \sqrt{|z| e^{i\theta}} = \sqrt{|z|} e^{i\theta/2} & (1) \\ \sqrt{|z| e^{i\theta + 2\pi}} = \sqrt{|z|} e^{i\theta/2 + \pi} & (2) \end{cases} \)

式 (1) を平方根の主値といいます。角度は \(2\pi\) を足すと同じ角度になるので、式 (2) がもう一つの解になります。三角関数の半角の公式を使うと、式 (1) から次の式が導かれます。

三角関数の半角の公式
\(\begin{eqnarray} \sin^2{\left(\frac{\theta}{2}\right)} = \dfrac{1 - \cos \theta}{2} \\ \cos^2{\left(\frac{\theta}{2}\right)} = \dfrac{1 + \cos \theta}{2} \end{eqnarray}\)
\(y \geq 0\) の場合
\(\begin{eqnarray} \sqrt{|z|} e^{i\theta/2} &=& \sqrt{|z|} \left(\cos{\frac{\theta}{2}} + i \sin{\frac{\theta}{2}}\right) \\ &=& \sqrt{|z|} \left(\sqrt{\frac{1 + \cos \theta}{2}}) + i \sqrt{\frac{1 - \cos \theta}{2}}\right) \\ &=& \dfrac{\sqrt{|z| + |z|\cos \theta}}{2} + i \sqrt{\frac{|z| - |z| \cos \theta}{2}} \\ &=& \sqrt{\frac{|z| + x}{2}} + i \sqrt{\frac{|z| - x}{2}}, \quad (|z|\cos \theta = x) \end{eqnarray}\)

\(y \lt 0\) の場合、虚部の符号が \(-\) になる
\( \sqrt{|z|} e^{i\theta/2} = \sqrt{\dfrac{|z| + x}{2}} - i \sqrt{\dfrac{|z| - x}{2}} \)

これをそのままプログラムすると、次のようになります。

リスト : 負数の判定

// 負のゼロか?
function is_negative_zero(n) {
    return n == 0.0 && 1.0 / n == -Infinity;
}

// 負数 (-0 を含む) か
function is_negative(n) {
    return n < 0.0 || is_negative_zero(n);
}
リスト : 平方根

    static sqrt(z) {
        let x = z.real;
        let a = z.abs();
        let b = Math.sqrt((a - x) / 2.0);
        return new Complex(Math.sqrt((a + x) / 2.0), is_negative(z.imag) ? -b : b);
    }

虚部の符号を判定するとき is_negative() を使うと、-0.0 にも対応することができます。

簡単な実行例を示します。

> console.log('%s', Complex.sqrt(new Complex(1, 0)))
C(1, 0)
undefined
> console.log('%s', Complex.sqrt(new Complex(2, 0)))
C(1.4142135623730951, 0)
undefined
> console.log('%s', Complex.sqrt(new Complex(3, 0)))
C(1.7320508075688772, 0)
undefined
> console.log('%s', Complex.sqrt(new Complex(-1, 0)))
C(0, 1)
undefined
> console.log('%s', Complex.sqrt(new Complex(-2, 0)))
C(0, 1.4142135623730951)
undefined
> console.log('%s', Complex.sqrt(new Complex(-3, 0)))
C(0, 1.7320508075688772)
undefined

> var a = [new Complex(0, 1), new Complex(0, -1), new Complex(1, 1), new Complex(1, -1)]
undefined
> for (x of a) console.log('%s', Complex.sqrt(x))
C(0.7071067811865476, 0.7071067811865476)
C(0.7071067811865476, -0.7071067811865476)
C(1.09868411346781, 0.4550898605622274)
C(1.09868411346781, -0.4550898605622274)
undefined

> for (x of a) {
... let y = Complex.sqrt(x);
... console.log('%s', y.mul(y));
... }
C(0, 1.0000000000000002)
C(0, -1.0000000000000002)
C(1, 1.0000000000000002)
C(1, -1.0000000000000002)
undefined

\(\sqrt x\) は \(\log x\) と同じ分枝切断を持っています。x を負の整数とすると sqrt(x) の解は \(i \sqrt x\) になりますが、もうひとつ \(-i \sqrt x\) という解があります。

> console.log('%s', Complex.sqrt(new Complex(-2, 0)))
C(0, 1.4142135623730951)
undefined
> console.log('%s', Complex.sqrt(new Complex(-2, -0)))
C(0, -1.4142135623730951)
undefined
> console.log('%s', Complex.sqrt(new Complex(-1e300, 0)))
C(0, 1e+150)
undefined
> console.log('%s', Complex.sqrt(new Complex(-1e300, -0)))
C(0, -1e+150)
undefined

このほかにも、複素数の逆三角関数や逆双曲線関数もプログラムすることができます。興味のある方は拙作のページ「お気楽 Python3 プログラミング超入門: 複素数」をお読みください。


●プログラムリスト

//
// complex.js : 複素数
//
// Copyright (c) 2025 Makoto Hiroi
//
// Released under the MIT license
// https://opensource.org/license/mit/
//

// 負のゼロか?
function is_negative_zero(n) {
    return n == 0.0 && 1.0 / n == -Infinity;
}

// 負数 (-0 を含む) か
function is_negative(n) {
    return n < 0.0 || is_negative_zero(n);
}

// クラス定義
class Complex {
    constructor(a = 0, b = 0) {
        this._real = a;
        this._imag = b;
    }
    get real() { return this._real; }
    get imag() { return this._imag; }

    // 等値
    equals(other) {
        return this.real == other.real && this.imag == other.imag;
    }
    // 文字列
    toString() { return `C(${this.real}, ${this.imag})`; }

    // 複素共益
    conj() { return new Complex(this.real, -this.imag); }
    // 偏角
    arg() { return Math.atan2(this.imag, this.real); }
    // 絶対値
    abs() { return Math.hypot(this.real, this.imag); }

    //
    // 四則演算
    //

    // 加算
    add(other) {
        return new Complex(this.real + other.real, this.imag + other.imag);
    }
    // 減算
    sub(other) {
        return new Complex(this.real - other.real, this.imag - other.imag);
    }
    // 乗算
    mul(other) {
        let {_real: xr, _imag: xi} = this;
        let {_real: yr, _imag: yi} = other;
        return new Complex(xr * yr - xi * yi, xr * yi + xi * yr);
    }
    // 除算
    div(other) {
        let {_real: xr, _imag: xi} = this;
        let {_real: yr, _imag: yi} = other;
        if (Math.abs(yr) >= Math.abs(yi)) {
            let u = yi / yr;
            let v = yr + yi * u;
            return new Complex((xr + xi * u) / v, (xi - xr * u) / v);
        } else {
            let u = yr / yi;
            let v = yr * u + yi;
            return new Complex((xr * u + xi) / v, (xi * u - xr) / v);
        }
    }

    //
    // 複素関数
    //

    // 指数関数
    static exp(x) {
        let e = Math.exp(x.real);
        return new Complex(e * Math.cos(x.imag), e * Math.sin(x.imag));
    }
    // 対数関数
    static log(x) {
        return new Complex(Math.log(x.abs()), x.arg());
    }
    // べき乗
    static pow(x, y) {
        return Complex.exp(y.mul(Complex.log(x)));
    }

    // 三角関数
    static sin(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.sin(x) * Math.cosh(y), Math.cos(x) * Math.sinh(y));
    }

    static cos(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.cos(x) * Math.cosh(y), - Math.sin(x) * Math.sinh(y));
    }

    static tan(z) {
        let x = z.real;
        let y = z.imag;
        let d = Math.cos(2.0 * x) + Math.cosh(2.0 * y);
        return new Complex(Math.sin(2.0 * x) / d, Math.sinh(2.0 * y) / d);
    }

    // 双曲線関数
    static sinh(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.sinh(x) * Math.cos(y), Math.cosh(x) * Math.sin(y));
    }

    static cosh(z) {
        let x = z.real;
        let y = z.imag;
        return new Complex(Math.cosh(x) * Math.cos(y), Math.sinh(x) * Math.sin(y));
    }

    static tanh(z) {
        let x = z.real;
        let y = z.imag;
        let d = Math.cosh(2.0 * x) + Math.cos(2.0 * y);
        return new Complex(Math.sinh(2.0 * x) / d, Math.sin(2.0 * y) / d);
    }

    // 平方根
    static sqrt(z) {
        let x = z.real;
        let a = z.abs();
        let b = Math.sqrt((a - x) / 2.0);
        return new Complex(Math.sqrt((a + x) / 2.0), is_negative(z.imag) ? -b : b);
    }
}

export default Complex;

初版 2025 年 1 月 18 日