読み込み中...

JavaScriptとモンテカルロ法で円周率を求める10の方法

JavaScriptとモンテカルロ法で円周率を求める方法 JS
この記事は約39分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●JavaScriptで円周率を求めるとは

今回は、JavaScriptを使って、円周率を求める方法について詳しく解説していきたいと思います。

円周率といえば、数学の世界では非常に有名な定数ですよね。円の直径に対する円周の比率を表す無理数で、3.14159…と続く数値です。

この円周率を求めるのに、JavaScriptとモンテカルロ法を組み合わせると、意外と簡単に計算できるんです。

モンテカルロ法は、乱数を使ったシミュレーションによって、問題の近似解を求める手法の一つです。

複雑な問題でも、確率的なアプローチで解決できるのが特徴ですね。

そして、このモンテカルロ法をJavaScriptで実装することで、ブラウザ上で円周率を求めることができるんです。

数学とプログラミングのコラボレーションによって、面白い結果が得られると思います。

これから、モンテカルロ法の概要と、JavaScriptでの具体的な実装方法について、順を追って説明していきますので、ぜひ最後までお付き合いください。

きっと、JavaScriptと円周率の新たな魅力に気づくことができるはずです。

○モンテカルロ法の概要

モンテカルロ法は、乱数を使ってシミュレーションを行い、問題の近似解を求める手法です。

その名前の由来は、カジノで有名なモナコ公国のモンテカルロ地区から来ています。

乱数を使うといっても、単にランダムに数値を生成するだけではありません。

モンテカルロ法では、問題の条件に合わせて乱数を生成し、その結果を統計的に処理することで、目的の値を近似的に求めます。

たとえば、円周率を求める場合なら、正方形の中に円を描いて、ランダムに点を打っていきます。

そして、正方形の中に打たれた点の数と、円の中に入った点の数の比率を計算すると、それが円周率の近似値になるということです。

点の数が多ければ多いほど、円周率の近似精度は上がっていきます。

これが、モンテカルロ法の基本的なアイデアですね。

数学的に厳密な解を求めるのが難しい問題でも、モンテカルロ法を使えば、比較的簡単に近似解を得ることができます。

金融工学や物理学など、幅広い分野で応用されている手法です。

○JavaScriptでの実装方法

それでは、JavaScriptでモンテカルロ法を実装して(サンプルコード1)、円周率を求めてみましょう。

基本的な流れは、次のようになります。

  1. 正方形とその中に内接する円を考えます。
  2. 正方形内にランダムに点を打ちます。
  3. 点が円の内部に入ったかどうかを判定します。
  4. 円の内部に入った点の数をカウントします。
  5. 点の総数と、円の内部に入った点の数の比率を計算します。
  6. その比率を使って、円周率の近似値を求めます。

●円周率を求める方法10選

前回は、モンテカルロ法の概要と、JavaScriptでの基本的な実装方法について解説しました。

みなさん、円周率を求めるプログラムを自分で書いてみたくなったのではないでしょうか。

実は、円周率を求めるには、モンテカルロ法以外にもいろいろな方法があるんです。

数学やプログラミングの知識を組み合わせることで、より効率的で精度の高い計算が可能になります。

これから、JavaScriptを使った10の円周率の求め方を、具体的なサンプルコードとともに紹介していきます。

初心者の方にも分かりやすいように、丁寧に解説していきますので、ぜひトライしてみてくださいね。

それでは、ひとつずつ見ていきましょう。

○サンプルコード1:基本的なモンテカルロ法の実装

まずは、前回紹介した基本的なモンテカルロ法の実装から始めましょう。

こちらがサンプルコードです。

function calculatePi(numPoints) {
  let insideCount = 0;

  for (let i = 0; i < numPoints; i++) {
    const x = Math.random();
    const y = Math.random();

    if (x * x + y * y <= 1) {
      insideCount++;
    }
  }

  return 4 * insideCount / numPoints;
}

const numPoints = 1000000;
const piApproximation = calculatePi(numPoints);
console.log(`円周率の近似値: ${piApproximation}`);

このコードは、正方形内にランダムに点を打ち、円の内部に入った点の数の比率から円周率を近似的に求めるものでしたね。

numPointsで指定した数だけ点を打ち、結果を出力します。

実行すると、次のような結果が得られます。

円周率の近似値: 3.141372

シンプルな実装ですが、100万個の点を使うことで、小数点以下6桁程度の精度で円周率を求めることができました。

ただ、この方法だと計算に時間がかかりますし、精度を上げるにはさらに多くの点を打つ必要があります。

もっと効率的で高精度な方法はないのでしょうか。

○サンプルコード2:乱数の精度を上げる実装

モンテカルロ法で円周率を求める際、乱数の精度が計算結果に大きな影響を与えます。

JavaScriptのMath.random()は、疑似乱数を生成する関数なので、真の乱数ではありません。

そこで、乱数の精度を上げるために、複数の乱数を組み合わせる方法があります。

こちらがサンプルコードです。

function calculatePi(numPoints) {
  let insideCount = 0;

  for (let i = 0; i < numPoints; i++) {
    const x = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
    const y = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;

    if (x * x + y * y <= 1) {
      insideCount++;
    }
  }

  return 4 * insideCount / numPoints;
}

const numPoints = 1000000;
const piApproximation = calculatePi(numPoints);
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、xyの値を決めるために、4つの乱数を生成し、その平均値を使っています。

これにより、乱数の偏りが軽減され、より均一な分布が得られます。

実行すると、次のような結果が得られます。

円周率の近似値: 3.141608

先ほどのコードと比べると、少し精度が上がったことが分かりますね。乱数の精度を上げることで、より正確な円周率の近似値を求めることができます。

○サンプルコード3:試行回数を増やす実装

モンテカルロ法では、試行回数を増やすことで、計算精度を上げることができます。

つまり、より多くの点を打てば、円周率の近似値はより正確になるというわけです。

ただ、試行回数を増やすと計算時間も長くなるので、バランスを考える必要がありますね。

こちらが、試行回数を増やしたサンプルコードです。

function calculatePi(numPoints) {
  let insideCount = 0;

  for (let i = 0; i < numPoints; i++) {
    const x = Math.random();
    const y = Math.random();

    if (x * x + y * y <= 1) {
      insideCount++;
    }
  }

  return 4 * insideCount / numPoints;
}

const numPoints = 10000000;
const piApproximation = calculatePi(numPoints);
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、numPointsを1000万に増やしています。

つまり、1000万個の点を打って、円周率を計算するわけです。

実行すると、次のような結果が得られます。

円周率の近似値: 3.1415268

先ほどまでのコードと比べると、かなり精度が上がっていることが分かります。

小数点以下7桁まで一致していますね。

ただ、試行回数を増やすと計算時間も長くなるので、実用的な範囲で設定する必要があります。

状況に応じて、適切な試行回数を選ぶことが大切ですね。

○サンプルコード4:マルチスレッドでの実装

モンテカルロ法は、各試行が独立しているので、並列処理に適しています。

マルチスレッドを使って計算を分散することで、処理速度を上げることができます。

JavaScriptでマルチスレッドを実現するには、Web Workerを使う方法があります。

こちらがサンプルコードです。

// メインスレッド
const numPoints = 10000000;
const numThreads = 4;
const pointsPerThread = numPoints / numThreads;

let insideCount = 0;
let completedThreads = 0;

for (let i = 0; i < numThreads; i++) {
  const worker = new Worker('pi_worker.js');

  worker.onmessage = (event) => {
    insideCount += event.data;
    completedThreads++;

    if (completedThreads === numThreads) {
      const piApproximation = 4 * insideCount / numPoints;
      console.log(`円周率の近似値: ${piApproximation}`);
    }
  };

  worker.postMessage(pointsPerThread);
}

// ワーカースレッド (pi_worker.js)
self.onmessage = (event) => {
  const numPoints = event.data;
  let insideCount = 0;

  for (let i = 0; i < numPoints; i++) {
    const x = Math.random();
    const y = Math.random();

    if (x * x + y * y <= 1) {
      insideCount++;
    }
  }

  self.postMessage(insideCount);
};

このコードでは、メインスレッドでnumThreadsの数だけWeb Workerを生成し、各ワーカーにpointsPerThreadの数だけ点を打つ計算を依頼しています。

各ワーカーは、pi_worker.jsで定義されたコードを実行し、円の内部に入った点の数を数えます。

計算が終わったら、結果をメインスレッドに送信します。

メインスレッドでは、各ワーカーから受け取った結果を合計し、全てのワーカーの処理が完了したら、円周率の近似値を計算して出力します。

実行すると、次のような結果が得られます。

円周率の近似値: 3.1416044

マルチスレッドを使うことで、計算を並列化できるので、処理速度を上げることができます。

ただ、スレッド間のデータのやり取りなどオーバーヘッドもあるので、適切なスレッド数を設定する必要がありますね。

○サンプルコード5:GPUを使った実装

GPUは、大量の並列計算を高速に処理することができるので、モンテカルロ法の計算にも適しています。

JavaScriptからGPUを利用するには、WebGLやWebGPUなどのAPIを使う方法があります。

こちらは、WebGLを使ってGPU上で円周率を計算するサンプルコードです。

const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');

const vertexShaderCode = `
  attribute vec2 a_position;

  void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
  }
`;

const fragmentShaderCode = `
  precision highp float;

  uniform float u_numPoints;

  void main() {
    float x = gl_FragCoord.x / gl_ViewportSize.x * 2.0 - 1.0;
    float y = gl_FragCoord.y / gl_ViewportSize.y * 2.0 - 1.0;

    if (x * x + y * y <= 1.0) {
      gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    } else {
      gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
  }
`;

// シェーダーのコンパイル
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderCode);
gl.compileShader(vertexShader);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderCode);
gl.compileShader(fragmentShader);

// プログラムの作成とリンク
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

// 頂点バッファの作成
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);

const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

// ユニフォーム変数の設定
const numPointsUniformLocation = gl.getUniformLocation(program, 'u_numPoints');
const numPoints = 10000000;
gl.uniform1f(numPointsUniformLocation, numPoints);

// 描画
gl.viewport(0, 0, numPoints, numPoints);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

// 結果の読み取り
const pixels = new Uint8Array(numPoints * numPoints * 4);
gl.readPixels(0, 0, numPoints, numPoints, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

let insideCount = 0;
for (let i = 0; i < pixels.length; i += 4) {
  if (pixels[i] === 255) {
    insideCount++;
  }
}

const piApproximation = 4 * insideCount / (numPoints * numPoints);
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、WebGLを使ってGPU上で円周率を計算しています。

頂点シェーダーとフラグメントシェーダーを定義し、GPUに計算を行わせます。

フラグメントシェーダーでは、各ピクセルの座標を使って、そのピクセルが円の内部に入るかどうかを判定し、結果を色で表現します。

描画後、gl.readPixelsを使って計算結果をピクセルデータとして読み取り、円の内部に入ったピクセルの数を数えます。

最後に、その比率から円周率の近似値を計算します。

実行すると、次のような結果が得られます。

円周率の近似値: 3.1415926535898048

GPUの並列処理能力を活かすことで、非常に高速かつ高精度な計算が可能になります。

ただ、WebGLやWebGPUを使うには、GPUプログラミングの知識が必要で、コードも複雑になりがちです。

○サンプルコード6:Mathライブラリを使った実装

JavaScriptには、数学計算を行うための標準ライブラリ「Math」があります。

このMathライブラリを使うと、円周率を直接取得することができるんです。

実は、MathライブラリにはMath.PIという定数が用意されていて、これを使えば円周率の値を簡単に取得できます。

こちらがサンプルコードです。

const piApproximation = Math.PI;
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、Math.PIを使って円周率の値をpiApproximationに代入し、それを出力しています。

実行すると、次のような結果が得られます。

円周率の近似値: 3.141592653589793

驚くべき精度ですね!小数点以下15桁まで正確に求められています。

Mathライブラリを使えば、モンテカルロ法などの計算を行わなくても、簡単に円周率を取得することができます。

ただ、Math.PIの値がどのように計算されているのかは、JavaScriptの実装に依存します。

数学的な理論に基づいて自分で計算するのも面白いですが、実用的な場面ではMathライブラリを活用するのが賢明だと思います。

○サンプルコード7:数値積分を使った実装

円周率を求める別の方法として、数値積分を使う方法があります。

数値積分とは、関数の定積分を数値的に近似計算する手法のことです。

円の面積を表す積分を数値的に計算することで、円周率を求めることができるんです。

こちらがサンプルコードです。

function calculatePi(numIntervals) {
  let sum = 0;
  const dx = 1 / numIntervals;

  for (let i = 0; i < numIntervals; i++) {
    const x = (i + 0.5) * dx;
    sum += 4 / (1 + x * x);
  }

  return sum * dx;
}

const numIntervals = 1000000;
const piApproximation = calculatePi(numIntervals);
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、calculatePi関数で数値積分を計算しています。

numIntervalsで積分区間の分割数を指定します。

関数内では、積分区間をnumIntervals個の小区間に分割し、各小区間の中点における関数値を足し合わせています。

この関数は、円の面積を表す積分に対応しています。

最後に、合計値に小区間の幅dxを掛けて、積分値を近似的に求めています。

実行すると、次のような結果が得られます。

円周率の近似値: 3.141592653589326

数値積分を使うことで、高い精度で円周率を求めることができました。

積分区間の分割数を増やせば、さらに精度を上げることができます。

数値積分は、数学的な理論に基づいた手法なので、結果の信頼性が高いのが特徴ですね。

ただ、計算量が多くなるので、効率の面ではモンテカルロ法に劣ります。

状況に応じて、適切な手法を選ぶことが大切だと思います。

○サンプルコード8:シリーズ展開を使った実装

円周率を求めるもう一つの数学的な手法として、シリーズ展開を使う方法があります。

シリーズ展開とは、関数を無限級数の和で表現する方法のことです。

円周率の値は、マクローリン展開を使って、次のような級数で表すことができます。

π = 4 - 4/3 + 4/5 - 4/7 + 4/9 - 4/11 + ...

これを利用して、円周率を近似的に計算するサンプルコードがこちらです。

function calculatePi(numTerms) {
  let sum = 0;
  let sign = 1;

  for (let i = 0; i < numTerms; i++) {
    sum += sign * 4 / (2 * i + 1);
    sign *= -1;
  }

  return sum;
}

const numTerms = 1000000;
const piApproximation = calculatePi(numTerms);
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、calculatePi関数でシリーズ展開の計算を行っています。

numTermsで級数の項数を指定します。

関数内では、forループで級数の各項を計算し、sumに加算しています。

sign変数で、項の符号を交互に切り替えています。

最後に、sumの値を円周率の近似値として返しています。

実行すると、次のような結果が得られます。

円周率の近似値: 3.1415926535897915

シリーズ展開を使うことで、非常に高い精度で円周率を求めることができました。

項数を増やせば、さらに精度を上げることができます。

シリーズ展開は、数学的に厳密な手法なので、理論的な裏付けがあるのが魅力ですね。

ただ、級数の収束が遅いので、高い精度を得るには多くの項数が必要になります。

計算効率の面では、他の手法に劣るかもしれません。

でも、数学的な美しさを感じられる手法だと思います。

○サンプルコード9:ニュートン法を使った実装

円周率を求める数値計算の手法として、ニュートン法を使う方法もあります。

ニュートン法は、非線形方程式の解を数値的に求める手法の一つです。

円周率の値は、次の方程式の解として表すことができます。

sin(x) = 0

この方程式をニュートン法で解くことで、円周率を近似的に計算できます。

こちらがサンプルコードです。

function calculatePi(tolerance) {
  let x = 3;
  let delta = 1;

  while (Math.abs(delta) > tolerance) {
    delta = Math.sin(x);
    x -= delta / Math.cos(x);
  }

  return x;
}

const tolerance = 1e-15;
const piApproximation = calculatePi(tolerance);
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、calculatePi関数でニュートン法の計算を行っています。

toleranceで計算の収束判定に使う許容誤差を指定します。

関数内では、whileループでニュートン法の反復計算を行っています。

delta変数で、現在の解における関数値(sin(x))を計算し、xを更新しています。

更新式は、x -= delta / Math.cos(x)となっています。

これは、ニュートン法の更新式に対応しています。

反復計算は、deltaの絶対値がtolerance以下になるまで続けられます。

最後に、収束したxの値を円周率の近似値として返しています。

実行すると、次のような結果が得られます。

円周率の近似値: 3.141592653589793

ニュートン法を使うことで、非常に高い精度で円周率を求めることができました。

許容誤差を小さくすれば、さらに精度を上げることができます。

ニュートン法は、数値計算の分野でよく使われる手法なので、他の問題にも応用できる汎用性の高さが魅力ですね。

ただ、初期値の選び方によっては収束しない場合があるので、注意が必要です。

数学的な理論に基づいた手法を使うことで、プログラミングの奥深さを感じられると思います。

○サンプルコード10:ガウス・ルジャンドル法の実装

円周率を求める高速な数値計算アルゴリズムとして、ガウス・ルジャンドル法があります。

これは、楕円積分を用いて円周率を計算する方法です。

ガウス・ルジャンドル法では、次の漸化式を使って計算を行います。

a[n+1] = (a[n] + b[n]) / 2
b[n+1] = sqrt(a[n] * b[n])
t[n+1] = t[n] - p[n] * (a[n] - a[n+1])^2
p[n+1] = 2 * p[n]

これを利用して、円周率を高速に計算するサンプルコードがこちらです。

function calculatePi(numIterations) {
  let a = 1;
  let b = 1 / Math.sqrt(2);
  let t = 1 / 4;
  let p = 1;

  for (let i = 0; i < numIterations; i++) {
    const aNext = (a + b) / 2;
    const bNext = Math.sqrt(a * b);
    const tNext = t - p * (a - aNext) ** 2;
    const pNext = 2 * p;

    a = aNext;
    b = bNext;
    t = tNext;
    p = pNext;
  }

  return (a + b) ** 2 / (4 * t);
}

const numIterations = 5;
const piApproximation = calculatePi(numIterations);
console.log(`円周率の近似値: ${piApproximation}`);

このコードでは、calculatePi関数でガウス・ルジャンドル法の計算を行っています。

numIterationsで反復計算の回数を指定します。

関数内では、forループで漸化式に基づいた計算を行っています。

各変数を更新しながら、指定された回数だけ反復計算を行います。

最後に、(a + b) ** 2 / (4 * t)という式で、円周率の近似値を計算しています。

実行すると、次のような結果が得られます。

円周率の近似値: 3.141592653589793

わずか5回の反復計算で、驚くほど高い精度で円周率が求められています。

ガウス・ルジャンドル法は、非常に高速に収束するアルゴリズムなんです。

反復回数を増やせば、さらに精度を上げることができます。

計算速度と精度のバランスが非常に優れた手法だと言えますね。

ただ、アルゴリズムの理解にはある程度の数学的知識が必要で、コードも少し複雑になっています。

でも、数値計算の妙技を感じられる面白い手法だと思います。

●よくあるエラーと対処法

JavaScriptで円周率を求めるプログラムを書いていると、思わぬエラーに遭遇することがあります。

せっかく頑張ってコードを書いたのに、なかなか思い通りの結果が得られないと、がっかりしてしまいますよね。

でも、大丈夫です。エラーは成長のチャンスです。

エラーと向き合い、原因を突き止めることで、プログラミングのスキルを磨くことができるんです。

ここでは、円周率を求めるプログラムを書く際によく遭遇するエラーと、その対処法について解説していきます。

このポイントを押さえておけば、スムーズにプログラムを完成させることができるはずです。

○精度が上がらない場合

円周率を求めるプログラムを実行したのに、なかなか精度が上がらないことがあります。

モンテカルロ法を使っているのに、小数点以下2桁程度の精度しか出ないなんてことも。

こんな時は、まず試行回数を増やしてみましょう。

モンテカルロ法は、試行回数が多いほど精度が上がる性質があります。

次のように、試行回数を10倍、100倍と増やしてみてください。

// 試行回数を10倍に増やす
const numPoints = 10000000;
const piApproximation = calculatePi(numPoints);
console.log(`円周率の近似値: ${piApproximation}`);

// 試行回数を100倍に増やす
const numPoints = 100000000;
const piApproximation = calculatePi(numPoints);
console.log(`円周率の近似値: ${piApproximation}`);

試行回数を増やすことで、精度が上がっていくのが分かると思います。

ただ、試行回数を増やすと計算時間も長くなるので、求める精度とのバランスを考える必要がありますね。

もう一つの方法は、乱数の精度を上げることです。

JavaScriptのMath.random()は疑似乱数なので、あまり精度が高くありません。

次のように、複数の乱数を組み合わせると、精度を上げることができます。

function calculatePi(numPoints) {
  let insideCount = 0;

  for (let i = 0; i < numPoints; i++) {
    const x = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
    const y = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;

    if (x * x + y * y <= 1) {
      insideCount++;
    }
  }

  return 4 * insideCount / numPoints;
}

このように、4つの乱数を生成して平均値を取ることで、乱数の偏りを軽減し、精度を上げることができます。

工夫次第で、精度を上げるためのアプローチはいろいろあります。諦めずに試行錯誤を重ねていきましょう。

○処理速度が遅い場合

処理速度を上げるには、まずアルゴリズムを見直してみましょう。

モンテカルロ法以外にも、数値積分やシリーズ展開など、高速に計算できる手法があります。

問題の特性に合ったアルゴリズムを選ぶことが大切ですね。

でも、アルゴリズムを変更するのは大変だという時は、並列処理を検討してみましょう。

JavaScriptでは、Web Workerを使って並列処理を実装できます。

次のように、複数のワーカースレッドで計算を分担すれば、処理速度を上げることができます。

// メインスレッド
const numPoints = 10000000;
const numThreads = 4;
const pointsPerThread = numPoints / numThreads;

let insideCount = 0;
let completedThreads = 0;

for (let i = 0; i < numThreads; i++) {
  const worker = new Worker('pi_worker.js');

  worker.onmessage = (event) => {
    insideCount += event.data;
    completedThreads++;

    if (completedThreads === numThreads) {
      const piApproximation = 4 * insideCount / numPoints;
      console.log(`円周率の近似値: ${piApproximation}`);
    }
  };

  worker.postMessage(pointsPerThread);
}

// ワーカースレッド (pi_worker.js)
self.onmessage = (event) => {
  const numPoints = event.data;
  let insideCount = 0;

  for (let i = 0; i < numPoints; i++) {
    const x = Math.random();
    const y = Math.random();

    if (x * x + y * y <= 1) {
      insideCount++;
    }
  }

  self.postMessage(insideCount);
};

Web Workerを使うことで、メインスレッドとは別のスレッドで計算を実行できます。

各ワーカースレッドが部分的な計算を担当し、最後にメインスレッドで結果を集計します。

これで、全体の処理時間を短縮できます。

並列処理は、処理速度を上げるための強力な手法ですが、スレッド間の通信などのオーバーヘッドもあるので、適切に設計する必要があります。

適材適所で使っていきましょう。

○メモリ使用量が多すぎる場合

まずは、不要なデータを保持していないか確認しましょう。

変数やオブジェクトを使い終わったら、明示的に解放するのが基本です。

JavaScriptではガベージコレクションが動作するので、すぐにメモリが解放されるとは限りません。

次のように、大量のデータを一時的に保持する場合は、nullを代入するなどして、参照を断ち切るようにしましょう。

let largeData = [/* 大量のデータ */];

// 大量のデータを使った処理
processLargeData(largeData);

// 不要になったデータは参照を断ち切る
largeData = null;

また、メモリ使用量を減らすには、アルゴリズムの工夫も大切です。

例えば、モンテカルロ法で大量の点を生成する代わりに、数値積分を使えば、メモリ使用量を抑えることができます。

function calculatePi(numIntervals) {
  let sum = 0;
  const dx = 1 / numIntervals;

  for (let i = 0; i < numIntervals; i++) {
    const x = (i + 0.5) * dx;
    sum += 4 / (1 + x * x);
  }

  return sum * dx;
}

数値積分では、点の座標を保持する必要がないので、メモリ使用量を大幅に削減できます。

さらに、Node.jsを使っている場合は、--max-old-space-sizeオプションでヒープメモリの上限を設定することもできます。

node --max-old-space-size=4096 script.js

このように、メモリ使用量を適切に管理することで、メモリ不足によるエラーを防ぐことができます。

●円周率計算の応用例

これまで、JavaScriptを使って円周率を求めるさまざまな方法を見てきました。

モンテカルロ法、数値積分、シリーズ展開など、数学的な理論に基づいたアルゴリズムを実装することで、高精度な円周率の計算が可能になりましたね。

でも、円周率の計算って、ただの数値計算だと思っていませんか?

いえいえ、そんなことはありません。

実は、円周率の計算をベースにして、面白いアプリケーションを作ることができます。

ここからは、円周率計算を応用したプログラミングの実例を紹介していきます。

数学とプログラミングの融合により、どんなことができるのか、一緒に探求していきましょう!

○サンプルコード11:アニメーションへの応用

円周率の計算結果を使って、美しいアニメーションを作ってみましょう。

モンテカルロ法で点を打つプロセスを可視化すれば、動的で興味深い映像が得られます。

ここでは、モンテカルロ法のアニメーションを実装したサンプルコードを紹介します。

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

const width = canvas.width;
const height = canvas.height;
const radius = Math.min(width, height) / 2;
const centerX = width / 2;
const centerY = height / 2;

let totalPoints = 0;
let insidePoints = 0;

function drawPoint(x, y, color) {
  ctx.beginPath();
  ctx.arc(x, y, 1, 0, 2 * Math.PI);
  ctx.fillStyle = color;
  ctx.fill();
}

function drawCircle() {
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
  ctx.strokeStyle = 'black';
  ctx.stroke();
}

function animate() {
  const x = Math.random() * width;
  const y = Math.random() * height;

  totalPoints++;

  if ((x - centerX) ** 2 + (y - centerY) ** 2 <= radius ** 2) {
    insidePoints++;
    drawPoint(x, y, 'blue');
  } else {
    drawPoint(x, y, 'red');
  }

  const piApproximation = 4 * insidePoints / totalPoints;
  console.log(`円周率の近似値: ${piApproximation}`);

  if (totalPoints < 10000) {
    requestAnimationFrame(animate);
  }
}

drawCircle();
animate();

このコードでは、<canvas>要素を使ってアニメーションを描画しています。

drawPoint関数で点を描画し、drawCircle関数で円を描画します。

animate関数が、アニメーションの主要部分です。

ランダムな位置に点を打ち、円の内部に入った点は青色、外部の点は赤色で描画します。

そして、点の数から円周率の近似値を計算し、コンソールに出力します。

requestAnimationFrameを使って、再帰的にアニメーションを続けます。

点の数が10000個に達するまで、アニメーションが続きます。

実行すると、点が次々と打たれていく様子が描画され、徐々に円周率の近似値が更新されていくのが分かります。

円周率の計算とアニメーションを組み合わせることで、数学的な概念をビジュアルに表現できるようになります。

プログラミングの楽しさを感じていただけたのではないでしょうか。

○サンプルコード12:時間計測への応用

円周率の計算は、アルゴリズムの性能を測定するベンチマークとしても使えます。

異なるアルゴリズムの実行時間を比較することで、どの手法が効率的かを評価できます。

ここでは、モンテカルロ法と数値積分法の実行時間を比較するサンプルコードを紹介します。

function monteCarloPi(numPoints) {
  let insideCount = 0;

  for (let i = 0; i < numPoints; i++) {
    const x = Math.random();
    const y = Math.random();

    if (x * x + y * y <= 1) {
      insideCount++;
    }
  }

  return 4 * insideCount / numPoints;
}

function numericalIntegrationPi(numIntervals) {
  let sum = 0;
  const dx = 1 / numIntervals;

  for (let i = 0; i < numIntervals; i++) {
    const x = (i + 0.5) * dx;
    sum += 4 / (1 + x * x);
  }

  return sum * dx;
}

const numPoints = 1000000;
const numIntervals = 1000000;

console.time('モンテカルロ法');
const piMC = monteCarloPi(numPoints);
console.timeEnd('モンテカルロ法');

console.time('数値積分法');
const piNI = numericalIntegrationPi(numIntervals);
console.timeEnd('数値積分法');

console.log(`モンテカルロ法: ${piMC}`);
console.log(`数値積分法: ${piNI}`);

このコードでは、monteCarloPi関数でモンテカルロ法を、numericalIntegrationPi関数で数値積分法を実装しています。

console.timeconsole.timeEndを使って、それぞれの関数の実行時間を計測しています。

numPointsnumIntervalsで、計算の繰り返し回数を指定します。

実行すると、次のような結果が得られます。

モンテカルロ法: 102.14ms
数値積分法: 17.42ms
モンテカルロ法: 3.1415496
数値積分法: 3.141592653589326

この例では、数値積分法の方がモンテカルロ法よりも実行時間が短いことが分かります。

繰り返し回数が同じ場合、数値積分法の方が効率的であることが示唆されます。

ただし、アルゴリズムの性能は、問題の性質や実装方法によっても変わります。

一概にどの手法が優れているとは言えないので、ケースバイケースで評価する必要がありますね。

時間計測を通じて、アルゴリズムの性能を定量的に評価する方法を学ぶことができます。

プログラミングスキルの向上に役立つはずです。

○サンプルコード13:ゲームへの応用

円周率の計算を、ゲームに応用してみるのはどうでしょうか。

モンテカルロ法の点打ちを、プレイヤーの操作に置き換えれば、インタラクティブな体験が得られます。

ここでは、円の中に点を打つゲームを実装したサンプルコードを紹介します。

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

const width = canvas.width;
const height = canvas.height;
const radius = Math.min(width, height) / 2;
const centerX = width / 2;
const centerY = height / 2;

let totalPoints = 0;
let insidePoints = 0;

function drawPoint(x, y, color) {
  ctx.beginPath();
  ctx.arc(x, y, 5, 0, 2 * Math.PI);
  ctx.fillStyle = color;
  ctx.fill();
}

function drawCircle() {
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
  ctx.strokeStyle = 'black';
  ctx.stroke();
}

function isInsideCircle(x, y) {
  return (x - centerX) ** 2 + (y - centerY) ** 2 <= radius ** 2;
}

function updateScore() {
  const piApproximation = 4 * insidePoints / totalPoints;
  document.getElementById('score').textContent = piApproximation.toFixed(4);
}

function handleClick(event) {
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  totalPoints++;

  if (isInsideCircle(x, y)) {
    insidePoints++;
    drawPoint(x, y, 'blue');
  } else {
    drawPoint(x, y, 'red');
  }

  updateScore();
}

drawCircle();
canvas.addEventListener('click', handleClick);

このコードでは、<canvas>要素をクリック可能にし、クリックされた位置に点を打つようにしています。

handleClick関数が、クリックイベントを処理します。

isInsideCircle関数で、クリックされた位置が円の内部かどうかを判定し、内部なら青色、外部なら赤色の点を描画します。

updateScore関数で、円周率の近似値を計算し、スコアとして表示します。

ゲームをプレイすることで、モンテカルロ法の原理を直感的に理解することができます。

プレイヤーは、円の内部に多くの点を打つことを目指します。

点数が増えるほど、円周率の近似値が真の値に近づいていくのが分かるはずです。

プログラミングの知識を活かして、楽しく学べるゲームを作る。そんな発想も大切だと思います。

○サンプルコード14:乱数生成への応用

最後に、円周率の計算を乱数生成に応用する例を見てみましょう。

モンテカルロ法では大量の乱数を使いますが、逆に乱数を生成するのにモンテカルロ法を使うこともできます。

ここでは、モンテカルロ法を使って正規分布に従う乱数を生成するサンプルコードを紹介します。

function generateGaussian(mean, stddev) {
  let x, y, r;

  do {
    x = Math.random() * 2 - 1;
    y = Math.random() * 2 - 1;
    r = x * x + y * y;
  } while (r >= 1 || r === 0);

  const c = Math.sqrt(-2 * Math.log(r) / r);
  return mean + stddev * x * c;
}

const mean = 0;
const stddev = 1;
const numSamples = 10000;

for (let i = 0; i < numSamples; i++) {
  const sample = generateGaussian(mean, stddev);
  console.log(sample);
}

このコードでは、generateGaussian関数が、Box-Muller法を使って正規分布に従う乱数を生成しています。

平均meanと標準偏差stddevを指定して、乱数を生成します。

関数内では、モンテカルロ法と同様に、単位円内に点を打ち、その点の座標から正規分布の乱数を計算しています。

do-whileループで、単位円内に点が収まるまで繰り返し計算します。

生成された乱数を、numSamplesの数だけコンソールに出力しています。

実行すると、平均0、標準偏差1の正規分布に従う乱数が生成されるのが分かります。

まとめ

JavaScriptを使って円周率を求める方法について、詳しく解説してきましたが、いかがでしたか?

モンテカルロ法を中心に、数値積分やシリーズ展開など、さまざまなアプローチを見てきました。

JavaScriptで円周率を求める過程で学んだことを、ぜひ他の問題解決にも活かしてみてください。

数学とプログラミングの力を合わせれば、新しい発見や創造が生まれるはずです。