この記事は、「Qiita全国学生対抗戦 Advent Calendar 2024」 シリーズ3 12月3日の記事です。
綾坂ことです。趣味でJavaScript狂人をやっている大学1年生です。
JavaScriptで3次方程式を解かなきゃいけなくなったので、複素数を扱うためのコードを書きました。
この記事はその実装で詰まったところとその解決方法の備忘録です。
完成品は"Zahlen.js"という名前で公開しています。実装だるいな〜って人は使ってください。
- 数学ⅡB以上未履修人間が雰囲気でやってます
- 数学的に間違ってることをやってたら教えて下さい、とくに複素数の扱い
- 自然数を$\mathbb{N}$、非負整数を$\mathbb{Z}_{\ge0}$、整数を$\mathbb{Z}$、有理数を$\mathbb{Q}$、ガウス有理数を$\mathbb{Q}\mathrm{(i)}$で表すことがあります
- ガウス有理数は実部と虚部がともに有理数な複素数
- 「ガウス有理数」のことを「複素数」って言ったりします
- めんどいので
クラスの作成
まあ$\mathbb{Z}\in\mathbb{Q}\in\mathbb{Q}\mathrm{(i)}$だと思うのでガウス有理数型を作ってそれを継承すればいいでしょう
- 実部分母・実部分子・虚部分母・虚部分子の4つのプロパティを持つガウス有理数クラスを作ります
- 各プロパティはbigintで持っておきましょう、numberだと絶対値$2^{53}$以上で正確性が吹っ飛びます
- 実部と虚部はそれぞれ既約分数に揃えておきます
- 符号は分子側に揃えておくことにします
- 継承は
super()
を使います
既約分数に揃えるためには分母と分子を最大公約数で割ればいいです。最大公約数を求める方法はユークリッドの互除法。
const gcd = (a, b) => {
if (b === 0n) return a; // bが0ならaを返せばOK
if (a < b) return gcd(b, a); // a ≧ bにそろえておく
return gcd(b, a % b); // bとaをbで割ったあまりをgcd()自身にわたす
}
JavaScript標準のMathはbigint非対応のため、abs()
とsign()
を自力で作っておく必要があります。
まあ比較演算子でやるだけなので説明する必要はないでしょう
number・stringへの変換
numberへの変換は.valueOf()
、stringへの変換は.toString()
を実装すればあとはいい感じにやってくれます
const Example = class Example {
constructor(a, b) {
this.a = a;
this.b = b;
}
valueOf() {
return this.a / this.b;
}
}
console.log(Number(new Example(3, 2))); // Expected Log Output : <number> 1.5
numberから有理数への変換
JavaScriptのnumberはIEEE 754 倍精度浮動小数点数なので、それをそのまま変換してあげるのがいいでしょう。
浮動小数点数がどういう形式で表しているかはWikipediaみるのが早いです
- numberを内部表現のビット列に変換するのはDataViewを使うのが楽っぽそうです
- まああとはやるだけといってもいいと思います
数学関数の実装
ECMAScript標準のnumberはMath
でいろんな計算ができるので、そこにあるやつはなるべく実装したいですね
数学定数
number
を有理数に変換できるのでMath
の定数を借りれますね!やったーー!
端数処理
$\mathbb{Q}\mathrm{(i)}$をどうするか迷いましたが、実部と虚部に対してそれぞれ端数処理をすることにしました。
実部と虚部をそれぞれ有理数で表せば有理数範囲の実装をするだけでOK。
有理数範囲の実装はtrunc()
をとっかかりにするのが楽でしょう、bigintに対して/
を使うとtrunc()
と同じ挙動になるので。
trunc()
ができればあとは0と正と負で場合分けしたらいけます
関数 | 0の場合 | 正の場合 | 負の場合 |
---|---|---|---|
ceil(x) |
0 | $\textrm{trunc}(x) + 1$ | $\textrm{trunc}(x)$ |
floor(x) |
0 | $\textrm{trunc}(x)$ | $\textrm{trunc}(x) - 1$ |
round(x) |
0 | $\lfloor{x+0.5}\rfloor$ | $\lceil{x-0.5}\rceil$ |
絶対値・符号
bigintのabs()
とsign()
を実装したはずなので、まあやるだけですね
クラス作成のタイミングで「符号は分子側に揃えておくことにする」といったのはここを楽にするためでもあります
複素数範囲のabs()
は三平方の定理を使う必要があります
どうせあとで有理数のhypot()
を実装すると思うのでそれの存在を仮定するのが楽でしょう
四則演算+α
add()
, sub()
, mul()
, div()
はKIT数学ナビゲーションをガン見してそのまま実装します、ありがとう金沢工業大学。
だからといって有理数や整数を↑でやるのは"鶏を割くに焉んぞ牛刀を用いん"案件なのでそれは個別実装。こっちは義務教育範囲のはずです
有理数(実数)範囲のmod()
は正負で場合分けするのが楽だと思います
- 任意の実数x, yについて$x \textrm{ mod } y$は$x$が正かどうかで分岐させれば以下のようにできます
- $x - y \times \textrm{trunc}(\frac{x}{y}) (x\ge{0})$
- $\textrm{sign}(x)\times(|x| \textrm{ mod } |y|) (x\lt{0})$
複素数範囲のmod()
ってなんだよと思いましたがmathlogの記事でmodの拡張をやっている方を見つけたのでそれを参考にしました。
mathlog『mod演算を複素数に拡張する』(by おれの名は。)
$\textrm{mod}(x,m)=\frac{m}{2\pi i}\log(e^{\frac{2\pi }{m}ix})$らしいです、なんか色々未実装なものを使っていますがあとでどうにかなります
比較演算
JavaScriptは演算子オーバーロードができないので比較演算子も関数で実装する必要があります。
eq()
($=$), ne()
($\ne$), ge()
($\ge$), gt()
($\gt$), le()
($\le$), lt()
($\lt$)の6つを実装すべき。
eq()
は「実部分母・実部分子・虚部分母・虚部分子」の4プロパティをそれぞれ比較すればOKです。
それだけで済ませることができるようにするための既約分数にする処理
ne()
はeq()
の否定。
不等号系をどうするかですが、複素数範囲は絶対値で比較することにしてZahlen.Q
範囲のみ考えます。
lt()
は$\frac{\texttt{x分子}}{\texttt{x分母}}\lt \frac{\texttt{y分子}}{\texttt{y分母}} \iff \texttt{x分子} \times \texttt{y分母} \lt \texttt{y分子} \times \texttt{x分母}$になるはずです。
残り3つは適当に実装できます。「ge()
はlt()
の否定、gt()
はge() || eq()
、le()
はlt() || eq()
」など。
"複素数全体に通常の大小関係を入れることはできない"とのことなので不等号な4つは複素数を弾くようにしたほうがいいのかも。エラー出すならなんかは返したほうがいいかな〜と思って無理やり比較させましたが
最大・最小は一つずつ比較すればいいでしょう(サボり)
指数・対数・三角・逆三角・双曲線・逆双曲線
有理数範囲はnumberに変換してMathを借りましょう、めんどいので
強いて言及するなら「atan2
もあとで使うので実装しましょう」くらいだと思います
以降、複素数範囲に対応することだけ考えます
Wikipediaをガン見したらそこに答えが載ってました、ありがとうWikipedia
(自力で計算しようとして4日無駄にしたのは内緒)
-
複素指数関数は $e^z = \exp(z) = \exp(a+bi) = e^a(\cos b + i \sin b)$ です
- $e^a$ も $\sin b$ も $\cos b$ も 実数範囲なので実装を仮定していいです
-
複素対数関数は $\textrm{Log}z = \log_e|z|+i\textrm{Arg}z$です
- 後述する「偏角の主値Arg(
arg()
)」の存在を仮定します -
log2()
とlog10()
は底の変換公式でいいと思います
- 後述する「偏角の主値Arg(
-
複素三角関数は $\sin{z}=\frac{e^{iz}-e^{-iz}}{2i}$, $\cos{z}=\frac{e^{iz}+e^{-iz}}{2i}$です
- $\tan{z}$ は $\frac{\sin{z}}{\cos{z}}$ でいいです
- 複素逆三角関数は $\textrm{asin}x = -i \log_e{(ix+\sqrt{1-x^2})}$, $\textrm{acos}x = \frac{\pi}{2}-\textrm{asin}x$, $\textrm{atan}x = \frac{i}{2}[\log(1-ix) - \log(1+ix)]$です
-
複素双曲線関数は$\sinh{z}=\frac{e^{z}-e^{-z}}{2i}$, $\cosh{z}=\frac{e^{z}+e^{-z}}{2i}$です
- $\tanh{z}$ は $\frac{\sinh{z}}{\cosh{z}}$ でいいです
- 実数範囲の定義がそのまま複素数範囲でも使える
-
複素逆双曲線関数は $\textrm{asinh}z = \log_e{(z+\sqrt{z^2+1})}$, $\textrm{acosh}z = \log_e{(z+\sqrt{z+1}\sqrt{z-1})}$, $\textrm{atanh}z = \frac{1}{2}\log_e{\frac{1+z}{1-z}}$です
- めっちゃきれい
冪乗・冪根
pow()
の存在を仮定すれば、sqrt()
・cbrt()
・hypot()
はやるだけになりますね!
pow()
は $\textrm{pv }z^a = e^{(a \textrm{Log}z)}$で複素数範囲まで行けるらしいです。
……が、簡単に実装できるところは個別で実装してあげたほうが速いと思います。
$x^{-a} = \frac{1}{x^a}$、$(\frac{n}{d})^a = \frac{n^a}{d^a}$を使えば「有理数の整数乗」までは容易に実装可能なはず。
複素数関連
Pythonのcomplex型を参考にいくつかメソッドを追加しておくといいかな〜と思ったのでそれもやりました
- ラジアン ←→ 角度
- 偏角
- Wikipedia(ja)『複素数の偏角』を見る
- 偏角の主値は
atan2(imag, real)
でOK-
atan2
の範囲は $\pi \lt \textrm{atan2}(y, x) \le \pi$ で、arg
の主値の範囲と同じ
-
-
arg
とphase
の両方で使えるようにしておくといいかも
- 複素数平面 ←→ 極形式表現
-
Pythonの
cmath.polar()
と同rect()
に相当するものをつくると便利かも - polar(x) は (abs(x), phase(x)) に等しいです ← はい
- rect(r, phi) は complex(r * math.cos(phi), r * math.sin(phi)) に等しいです ← はい
- 私は名前を
orthogonal()
にしました
- 私は名前を
-
Pythonの
以上備忘録でした。
再掲になりますが書いたコードは公開してるので実装だるいな〜って人は使ってください。