円周率πの近似値を計算する過程(モンテカルロ法)を Flutter で可視化する

7 min読了の目安(約7100字TECH技術記事

ふと思い立って Flutter でモンテカルロ法を使って円周率の近似値が正しい値に向かっていく過程を可視化してみました。この記事ではモンテカルロ法の簡単な解説と、Flutter 実装の過程を解説していきます。

モンテカルロ法で円周率を導出できるロジック

まずモンテカルロ法(乱数を数値計算に使用する方法の総称)でどうやって円周率が求まるのかについての解説です。名前はいかついですが、実はとても簡単なロジックです。

前提

まず、前提として円の面積について、半径をrとすると r2πr^2π です。
それに対して半径が r の正方形の面積は 2r×2r=4r22r × 2r = 4r^2 です。
ここまでは大丈夫ですよね。さて、ここで、
円の面積と、正方形の面積の比率は、

πr2:4r2=π:4 πr^2 : 4r^2 = π : 4

となります。

モンテカルロ法

さてここからがモンテカルロ法です。
先ほど、円の面積と、正方形の面積の比率がわかりました。
そして、その中の未知数は、π だけです。
これはつまり、実際の円の面積と正方形の面積の比率さえわかれば π を計算できることになります。
しかしそれがわからないから困っているんじゃないか、という話ですよね。

でももし、ランダムに大量の二次元上の点を生成して、
原点からの距離が 1 の点の数と、全部の点の数を比較したら、
それって円の面積と正方形の面積の比率を疑似的に再現できそうだと思いませんか?
イメージを掴むには以下の可視化した図をご覧ください。
Monte Carlo
これがモンテカルロ法です。そして、
原点からの距離が 1 の点の数 を P
全部の点の数 を N
とすると、

P:Nπ:4 P : N ≒ π : 4

が成り立つことになります。これを数式に直すと、

Nπ4P Nπ ≒ 4P

となり、πの値が計算できます。

π4PN π ≒ \frac{4P}{N}

Flutter での実装

ロジックだけ実装しても面白くないので、
ランダムな点が増えていく過程を可視化してみよう、ということで、Flutter を使います。
主に Hooks Riverpod と Flutter Hooks を使用します。

定期的にランダム値を生成する

まずランダムな値に関しては、dart:math ライブラリの Random().nextDouble() 関数で生成できます。
Timer.periodicを使うと、定期的に行いたい処理を指定できます。
第一引数に、どの頻度かを Duration型で、
第二引数に、行いたい処理を関数で渡します。
Timer.periodic の戻り値は Timer 型で、止めたい時には cancel を呼びだせばできます。
これを useEffect の中で一度だけ読んであげれば OK。

    useEffect(() {
      Timer.periodic(
        const Duration(milliseconds: 500),
        (timer) {
	    // 500 ms 毎にランダムな点を追加
	    final x = random.nextDouble();
            final y = random.nextDouble();
	    // ランダム値が円に入っているかは、以下で判定
            final bool insideCircle = (x * x + y * y) < 1;
	    ...
        }
      );
   });

点を表示する

点を二次元上に表示します。
それには、CustomPoint を使う手もあると思いますが、ここでは、
Stack と Positioned の組み合わせで実現しました。
0-1 のランダム値に対して、以下の scaller をかけ合わせることで、点の位置がいい感じになります。

    final double width = MediaQuery.of(context).size.width;
    final double height = MediaQuery.of(context).size.height;
    final double scaller = min(width, height);

実行結果

全ソースコードはこちら

こちらが使用した全ソースになります。
ちなみに処理を止める部分は書き忘れたので、
メモリを食いつぶしてクラッシュするまで続きます😂
以上、Flutter は可視化ツールにもいい感じに使えるというお話しでした。

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:intl/intl.dart';

final pointsInsideState = StateProvider<List<Point>>((ref) => []);
final pointsOutsideState = StateProvider<List<Point>>((ref) => []);
final piState = StateProvider<double>((ref) {
  final pointsOutside = ref.watch(pointsOutsideState).state;
  final pointsInside = ref.watch(pointsInsideState).state;
  final n = pointsOutside.length + pointsInside.length;
  final p = pointsInside.length;
  return p * 4.0 / n;
});

class Point {
  Point(this.x, this.y);
  final double x;
  final double y;
}

class RedCircle extends StatelessWidget {
  const RedCircle();
  
  Widget build(BuildContext context) {
    return Container(
      width: 10,
      height: 10,
      decoration: const BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.red,
      ),
    );
  }
}

class BlueCircle extends StatelessWidget {
  const BlueCircle();
  
  Widget build(BuildContext context) {
    return Container(
      width: 10,
      height: 10,
      decoration: const BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.blue,
      ),
    );
  }
}

final random = Random();
final f = NumberFormat('###.0####');

class Montecarlo extends HookWidget {
  
  Widget build(BuildContext context) {
    final double width = MediaQuery.of(context).size.width;
    final double height = MediaQuery.of(context).size.height;
    final double scaller = min(width, height);
    useEffect(() {
      Timer.periodic(
        const Duration(milliseconds: 500),
        (timer) {
          final pointsOutside = context.read(pointsOutsideState).state;
          final pointsInside = context.read(pointsInsideState).state;
          final repeat = pointsOutside.isEmpty && pointsInside.isEmpty
              ? 1
              : pointsOutside.length + pointsInside.length;
          for (int i = 0; i < repeat; i++) {
            final x = random.nextDouble();
            final y = random.nextDouble();
            final bool insideCircle = (x * x + y * y) < 1;
            final point = Point(x * scaller, y * scaller);
            if (insideCircle) {
              pointsInside.add(point);
            } else {
              pointsOutside.add(point);
            }
          }
          context.read(pointsOutsideState).state = pointsOutside;
          context.read(pointsInsideState).state = pointsInside;
        },
      );
      return;
    }, []);
    final double pi = useProvider(piState).state;
    final String piStr = f.format(pi);
    final List<Point> pointsInside = useProvider(pointsInsideState).state;
    final List<Point> pointsOutside = useProvider(pointsOutsideState).state;
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        width: scaller,
        height: scaller,
        child: Stack(
          children: [
            ...pointsOutside.map(
              (p) => Positioned(
                left: p.x,
                top: p.y,
                child: const RedCircle(),
              ),
            ),
            ...pointsInside.map(
              (p) => Positioned(
                left: p.x,
                top: p.y,
                child: const BlueCircle(),
              ),
            ),
            Container(
              padding: const EdgeInsets.only(left: 20, bottom: 20),
              child: Align(
                alignment: Alignment.bottomLeft,
                child: Row(children: [
                  Text(
                    'N: ${pointsInside.length + pointsOutside.length},',
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(width: 20),
                  Text(
                    'P: ${pointsInside.length},',
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(width: 20),
                  Text(
                    'π = P * 4 / N = $piStr',
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ]),
              ),
            ),
            Center(
                child: Text(
              '${piStr}',
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                color: Colors.white,
                fontSize: 38,
              ),
            )),
          ],
        ),
      ),
    );
  }
}