ラベル 円周率 の投稿を表示しています。 すべての投稿を表示
ラベル 円周率 の投稿を表示しています。 すべての投稿を表示

2025年7月7日月曜日

円周率を数値計算する

誰もが小学生の時に友達とこぞって暗記する数学の超有名定数こと円周率ですが、みなさんは円周率を数値計算したいと思ったことは無いですか?私は日ごろからしてみたいと思っていました(?)。

パソコンで数値計算をしようとしたら、超有名ソフト「SUPER PI」や、現在でも速度面で覇権をとっている「y-cruncher」などが有名ですね。当然ながらそういったソフトに勝てるようなソフトは作れないのですが、円周率の数値計算の世界を少し覗いてみましょう。

逆正接関数(arctan)のテイラー展開

数値計算といったらテイラー展開です。過去にネイピア数を求めたときもテイラー展開でしたね。円周率の数値計算の話をすると、arctan(tanの逆関数)やarcsin(sinの逆関数)を使う方法が2番目くらいに出てくるかと思います(1番目は内接多角形・外接多角形の話かなと思います。個人の意見です)。というわけで、早速arctanのテイラー展開に取り組んでいきましょう。

まず、唐突ですがarctanの微分を考えます。 

\[y=\arctan x\]

と置くと、定義より 

\[x=\tan y\] 

両辺微分して 

\[\frac{dx}{dy}=\frac{d}{dy}\tan y =\frac{1}{\cos^2 y}\]  

よって

\[\frac{dy}{dx}=\cos^2 y=\frac{1}{1+\tan^2 y}\]  

ここで$y=\arctan x$を代入すると

\[\frac{dy}{dx}=(\arctan x)'=\frac{1}{1+x^2}\]  

arctanの微分って意外とシンプルな表現になるんですね。両辺積分して、arctanの形で表現するようにしておきましょう。

\[\arctan x=\int_{0}^{x} \frac{1}{1+t^2} dt\]   

ここで、さらに唐突ですが、無限等比級数の公式を思い出します。

\[ 1+r+r^2+r^3+\cdots=\frac{1}{1-r} \qquad (|r|<1) \]

これに$r=-t^2$を代入すると

\[ 1-t^2+t^4-t^6+\cdots=\frac{1}{1+t^2} \qquad (t^2<1) \] 

あら不思議、右辺は先ほどのarctanの積分表記と同じですね。ということで代入して、

\[ \begin{align} \arctan x &=\int_{0}^{x} \frac{1}{1+t^2} dt \notag \\ &=\int_{0}^{x} (1-t^2+t^4-t^6+\cdots) dt \notag \\ &=\left [t-\frac{t^3}{3}+\frac{t^5}{5}-\frac{t^7}{7}+\cdots \right ]_{0}^{x} \notag \\ &= x-\frac{x^3}{3}+\frac{x^5}{5}-\frac{x^7}{7}+\cdots \notag \end{align} \]

無事テイラー展開ができました。

ついでに円周率を求めておきましょう。arctanの定義より、 

\[ \arctan 1 = \frac{\pi}{4} \]

となるので、テイラー展開を適用して、

\[ \frac{\pi}{4} = 1-\frac{1}{3}+\frac{1}{5}-\frac{1}{7}+\cdots \]

という級数が得られます(途中式で$t^2<1$という条件があったから$x=1$を入れられないじゃないかって?収束半径とかごにょごにょ改めて計算すると大丈夫らしいです)。見ての通り、この級数を計算して4倍すれば円周率ですね。

この級数はとてつもなく収束が遅いことで知られています。10桁計算するのに100億項まで計算する必要があるんだとか。そりゃ、分母が等差数列でしか増えていかなくて、+と-が交互に出てくるんじゃ収束は遅いですよね。

マチンの公式

1706年にイギリスの天文学者マチンは、次の公式を発表しました。

\[ \frac{\pi}{4} = 4 \arctan\frac{1}{5}-\arctan\frac{1}{239} \]

これを上のテイラー展開の式に当てはめて考えてみましょう。

\[ \frac{\pi}{4} = 4 \left (\frac{1}{5}-\frac{1}{3 \cdot 5^3}+\frac{1}{5 \cdot 5^5}-\frac{1}{7 \cdot 5^7}+\cdots \right) - \left (\frac{1}{239}-\frac{1}{3 \cdot 239^3}+\frac{1}{5 \cdot 239^5}-\frac{1}{7 \cdot 239^7}+\cdots \right ) \] 

$x^{2n}$で増えていく項が分母に付いたので、収束がぐんと早くなることは想像に難くありません。実際、この方法だと10桁を計算するのに10項ほどで事足ります。

せっかくなので、この公式も導出しておきましょう。

$\theta=\arctan(1/5)$と置いて、2倍角の公式を2回使って$\tan 4\theta$を求めます。

\[ \tan 2\theta = \frac{2 \tan \theta}{1-\tan^2 \theta} = \frac{2 \cdot \frac{1}{5}}{1-\frac{1}{25}}=\frac{5}{12} \]

\[ \tan 4\theta = \frac{2 \tan 2\theta}{1-\tan^2 2\theta} = \frac{2 \cdot \frac{5}{12}}{1-\left(\frac{5}{12}\right)^2}=\frac{120}{119} \] 

ここで、本命となる$\tan(4\theta-\pi/4)$を計算します。加法定理より 

\[ \tan \left(4\theta-\frac{\pi}{4}\right) = \frac{\tan 4\theta - \tan\frac{\pi}{4}}{1+\tan 4\theta \tan\frac{\pi}{4}}=\frac{\frac{120}{119}-1}{1+\frac{120}{119}}=\frac{1}{239}\]  

よって

\[ 4\theta-\frac{\pi}{4} = \arctan \frac{1}{239} \] 

\[ \frac{\pi}{4} = 4\arctan\frac{1}{5}-\arctan \frac{1}{239} \]  

実装

さて、マチンの公式を実装していきましょう。

static void Main()
{
    int digits = 100;
    int scale = digits + 10; // 十分な精度確保のため余裕を持たせる

    var pi = 16 * Arccot(5, scale) - 4 * Arccot(239, scale);
    var piText = ToDecimal(pi, scale).Substring(0, digits + 2);

    Console.WriteLine(piText);
}

static BigInteger Arccot(int x, int scale)
{
    BigInteger unity = BigInteger.Pow(10, scale);
    BigInteger xPower = unity / x;
    BigInteger sum = xPower;
    BigInteger term;
    int n = 3;
    bool negative = true;
    long x2 = (long)x * x;

    while(true) {
        xPower /= x2;
        term = xPower / n;

        if(term == 0)
            break;

        sum += negative ? -term : term;
        negative = !negative;
        n += 2;
    }

    return sum;
}

static string ToDecimal(BigInteger value, int scale)
{
    string s = BigInteger.Abs(value).ToString().PadLeft(scale + 1, '0');
    int decimalPosition = s.Length - scale;
    return (value.Sign < 0 ? "-" : "") + s.Insert(decimalPosition, ".");
}

Arccot関数はいわゆる逆余接関数(コタンジェントの逆関数)です。要するに、正接の逆数を与えることになります。上のテイラー展開の式を見てもわかる通り、マチンの公式だと各項の分子は常に1ですので、コタンジェントと見立てれば引数はすべて整数にできるのですね。

また、計算の過程でも、例えば小数点以下第100桁まで求めたければ、ひとまず10^100を用意して、それを各項の分母で割って足していくことで級数の和を計算します。最終的に項の値が0になってしまったら、それ以降は1未満の項で無視できるということで計算を終了させています。これによって、分数を使わずにすべて整数として計算できるようになっています。

また、求めたい桁数に対して、10桁ほど多めに計算するようにしています。これは、テイラー展開の各項の計算における割り算の誤差や、最後に足し合わせた時の繰り上げ/繰り下げ等で多少の誤差が出ることから、少し多めの桁数を計算しているといったところです。本当はちゃんと挟みうちをして誤差を評価すべきでしょうが、まあ、除算や足し算の誤差なんて知れているので、この程度で概ね充分でしょう。

マチンの公式の拡張

さて、上のコードは充分にシンプルで良いものですが、例えば私のパソコンで実行すると、30万桁で約47秒かかります。もう少し良い方法は無いでしょうか?

実は、マチンの公式に類似した公式、すなわちarctanの中身が1/nであるものを足し合わせただけの公式は多数あります。当然分母が大きいほうが収束が早いはずですので、そのようなものがあればより早く計算できることが期待できます。

というわけで次の公式を使って計算してみましょう。

\[ \frac{\pi}{4}=44 \arctan \frac{1}{109}+95 \arctan \frac{1}{239}-12 \arctan \frac{1}{682}+24 \arctan \frac{1}{12943}-44 \arctan \frac{1}{6826318}\] 

すべての分母が100超えということで、計算速度に期待できますね。

また、項が5つありますが、各項は独立して計算できるので並列処理してしまいましょう。

static void Main()
{
    int digits = 100;
    int scale = digits + 10; // 十分な精度確保のため余裕を持たせる

    var members = new BigInteger[5];
    Parallel.Invoke(
        () => members[0] = 176 * Arccot(109, scale),
        () => members[1] = 380 * Arccot(239, scale),
        () => members[2] = -48 * Arccot(682, scale),
        () => members[3] = 96 * Arccot(12943, scale),
        () => members[4] = -176 * Arccot(6826318, scale)
    );
    var pi = members[0] + members[1] + members[2] + members[3] + members[4];
    var piText = ToDecimal(pi, scale).Substring(0, digits + 2);

    Console.WriteLine(piText);
}

これで30万桁計算すると、約15秒で計算が終わりました。マチンの公式に比べて3倍ほど早いですね。

項数が増えると各項を足し合わせるのに時間がかかるのでは?と思うかもしれませんが、多倍長整数の和は簡単に計算できるのでここはそんなにボトルネックになりません。arccotの計算にかかるが支配的で、その次は10進変換です。

まとめ

ということで、マチンの公式及びその亜種を使っての円周率の計算をやってきました。高校数学くらいで理解できる式で、簡単かつそれなりに計算できるものなのですね。

ちなみに、私のコードで30万桁15秒でしたが、SUPER PIだと52万桁で3秒でした。y-cruncherだと1億桁計算して5秒なので、足元にも及ばないですね…。

2021年11月23日火曜日

弾性衝突で円周率を求める話

最近、YouTubeでこんな動画を見かけました。

2つの物体を衝突させる話です。左端には壁があり、2つの物体を並べ、右側の物体を左に向かって滑らせます。摩擦はなく、すべての衝突が弾性衝突だとすると、左と右の物体の質量比が$100^n$のとき、左の物体の衝突回数は円周率の小数第$n$位までを整数で表したものになるというのです。

少し調べてみると比較的有名なことのようですが、私は初耳でした。私はうっかり解説動画から見てしまったので、自分で考える間もなくなぜそうなるかを知ってしまったのですが、もしも初めて聞いた人は答えを見ずに自分で考えてみることをお勧めします。何もヒントなしに証明できた人がいたとしたら大したものです。

自分で考えてみる 

さて、解説動画を見ているとところどころで天才的なひらめきが出てくるのですが、いったんそれは忘れて少し考えてみましょう。

右向きに$x$軸を取り、右側の物体を物体1とし、その質量と速度をそれぞれ$m_1$, $v_1$、左側の物体を物体2としk、その質量と速度をそれぞれ$m_2$, $v_2$と置きます。

こういうのはそれぞれの物体の位置と速度を計算するとドツボにはまります。使用すべきはエネルギー保存則と運動量保存則です。2つの物体が衝突するときは弾性衝突なので2つの物体のエネルギーの合計も運動量の合計も保存します。左の物体が壁にぶつかるときは、弾性衝突なのでエネルギーは保存しますが運動量は正負反転します。

まずは2つの物体が衝突するときのことを考えてみましょう。

\[\left\{
\begin{array}{l}
\dfrac{1}{2}m_1v_1^2+\dfrac{1}{2}m_2v_2^2=k \\
m_1v_1+m_2v_2=p
\end{array}
\right. \]

何の変哲もない、ただのエネルギー保存則と運動量保存則です。私は凡人なので、これを連立させて2次方程式を解こうとしました。$v_2$を削除し$v_1$を求めます。ただの2次方程式なので解の公式を使えば求められますが、結構計算量が多くしんどい計算となります。

\[v_1=\dfrac{p\pm{m_2\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p^2}}}{m_1+m_2}\]

ルートの中身を$m_1$と$m_2$が対称になるように整理するのがポイントです。これを運動量保存則の式に当てはめて$v_2$を計算します。

\[v_2=\dfrac{p\mp{m_1\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p^2}}}{m_1+m_2}\]

ルートの中身を対称にしたおかげでかなりきれいに計算できました。

この式が意味するところは、「2つの物体のエネルギーの合計が$k$、運動量の合計が$p$の場合、$v_1$と$v_2$の取りうる組み合わせは2ペアある」ということになります。これはすなわち物体の衝突前の状態と衝突後の状態です。衝突の後は物体1はより右向きの速度が上がりますので、下側の符号が衝突前、上側の符号が衝突後の速度ということになりますね。

さて、物体同士が衝突した後に物体2が壁にぶつかると物体2の運動量の正負が反転します。すなわち、物体1,2の運動量の合計としては

\[p=m_1v_1-m_2v_2\]

と表せます。これを使えば、物体1,2がぶつかってから物体2が壁にぶつかるという1サイクルでの運動量の変化を漸化式で表すことができるようになります。わかりやすく、サイクル開始時点での運動量を$p_n$、終了時点での運動量を$p_{n+1}$としてみましょう。

\[\begin{eqnarray}p_{n+1}&=&m_1\dfrac{p_n+m_2\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p_n^2}}{m_1+m_2}-m_2\dfrac{p_n-m_1\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p_n^2}}{m_1+m_2}\nonumber\\&=&\dfrac{(m_1-m_2)p_n+2m_1m_2\sqrt{2k(\frac{1}{m_1}+\frac{1}{m_2})-\frac{1}{m_1m_2}p_n^2}}{m_1+m_2}\nonumber\end{eqnarray}\]

さて、運動量$p$に関する漸化式ができました。この漸化式のnを数えれば衝突回数がわかるはずです(※1)。終了条件はもう衝突が起こらなくなるということで、それはすなわち運動量の絶対値が初期運動量の絶対値以上になったときと言えます(※2)。

※1:nが1増えるたびに衝突は2回起こります。
※2:これは正確には壁にぶつからなくなる条件で、この後に物体同士でぶつかるかは検証する必要があります。

シミュレーション

ここまで来たので、今までの計算が正しいことを確認するために実際にプログラムを書いて漸化式を数値的に解いてみます。

double n = 7;
double v1 = -1;
double v2 = 0;
double m1 = Math.Pow(10, n * 2);
double m2 = 1;

double p = m1 * v1 + m2 * v2;
double p0abs = Math.Abs(p);
double k = (m1 * v1 * v1 + m2 * v2 * v2) / 2;

uint collision = 0;

while(true) {
	var pprev = p;
	var sqrt = CalcSqrt(p, k, m1, m2);
	p = CalcNextMomentum(sqrt, p, m1, m2);
	collision += 2;

	if(Math.Abs(p) >= p0abs) {  // 合計運動量が初期運動量の反対向きを超えていたらもう壁にはぶつからない
		// -v2 > v1 ⇔ v1 + v2 < 0 だったらもう1回物体同士でぶつかる
		if(2 * pprev + (m2 - m1) * sqrt < 0)
			collision++;
		break;
	}
}

Console.WriteLine($"Pi = {collision / Math.Pow(10, n)}");


static double CalcSqrt(double p, double k, double m1, double m2)
{
	return Math.Sqrt(2 * k * (1 / m1 + 1 / m2) - 1 / (m1 * m2) * p * p);
}

static double CalcNextMomentum(double sqrt, double p, double m1, double m2)
{
	return ((m1 - m2) * p + 2 * m1 * m2 * sqrt) / (m1 + m2);
}

余談ですがC#10.0/.NET6.0になってnamespaceやMainメソッドが省略されるようになりました。このままのコピペでコンパイルが通ります。

運動量の漸化式を計算するメソッドがCalcNextMomentumですが、平方根部分のみを別途計算するCalcSqrtメソッドを用意しています。運動量を計算していって運動量の絶対値が初期運動量の絶対値以上となったらもうこれ以上の壁への衝突は発生しないものとして、物体同士で最後にぶつかるかどうかの判定へ移ります。

この時点での2つの物体の速度は、1回前のループで計算した運動量に対して計算した$v_1$と$-v_2$になるはずです。その大小関係ですので分子のみの差をとって判定しています。ここでルート部分が再利用できるため、メソッドを分けてわざわざルート部分の値を保存していたのです。

このプログラムは小数第7位までの計算($n=7$)まで正常に動きます。それ以上は浮動小数点型の有効桁数が足りずに正確に計算できず、いつまでたってもループを抜け出せなくなってしまいます。

漸化式を解く

この漸化式解けるのか…?

まとめ

というわけで、多分私がこの問題だけを見て答えを見ていなかったら、ここまで解いて詰んで終わっていたでしょう。あの「エネルギー保存則円の方程式運動量保存則直線の方程式とみたてて平面上に表すと、衝突回数が円周運動量保存則の直線傾き角の2倍割ったものと考えることができる」とかいう天才的なひらめき、いったいどういう練習をしたら思いつけるようになるんだか。