Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Monte Carlo Pi

monte-carlo-pi.livemd

Monte Carlo Pi

Mix.install([
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.10"}
])

Introduction

This notebook uses Monte Carlo simulation to approximate π.

By placing 2d points randomly witnin -1 to 1 on each axis and looking at the fraction that land within the unit circle, one can approximate π. The more points, the better the approximation.

Given $A{sqr}$ being the area of the square and $A{circ}$ being the area of the unit circle, we have that

$ \frac{A{circ}}{A{sqr}} = \frac{\pi r^2}{4r^2} = \frac{\pi}{4} $

or

$ \pi = 4 \cdot \frac{A{circ}}{A{sqr}} $

A longer description can be found here.

Note: The outcome is determined by chance and the quality of the employed random number generator.

Configuration

step_count = 2_000

Simulation

Produce coordinates:

samples =
  1..step_count
  |> Enum.map(fn _ -> %{x: 2 * :rand.uniform_real() - 1, y: 2 * :rand.uniform_real() - 1} end)

Map coordinates to whether they are inside of the unit circle:

outcomes =
  samples
  |> Enum.map(fn sample -> :math.sqrt(sample.x * sample.x + sample.y * sample.y) < 1 end)

Calculate the series of approximations:

approximation =
  outcomes
  |> List.foldl([], fn outcome, acc ->
    {count, inside, outside} =
      case acc do
        [last | _] -> {Map.get(last, "count"), Map.get(last, "inside"), Map.get(last, "outside")}
        _ -> {0, 0, 0}
      end

    {inside, outside} =
      case outcome do
        true -> {inside + 1, outside}
        false -> {inside, outside + 1}
      end

    entry = %{
      "count" => count + 1,
      "inside" => inside,
      "outside" => outside,
      "pi" => inside / (count + 1) * 4,
      "legend" => "simulated"
    }

    [entry | acc]
  end)
  |> Enum.reverse()

Result

The final approximation is:

approximation
|> Enum.at(-1)
|> (fn entry -> Map.get(entry, "pi") end).()

Simulation Set

defmodule Canvas do
  @dim 320
  @unit 256
  @point_r 2
  @gap 8
  @gridcolor "#999999"

  def render(samples) do
    grid_lines = grid()
    circle_lines = circle()
    point_lines = point(samples)

    """
    
    #{grid_lines}
    #{circle_lines}
    #{point_lines}
    
    """
    |> Kino.Image.new(:svg)
  end

  defp grid() do
    dashes = "stroke-dasharray=\"3\""

    """
    

    
    
    

    
    
    

    1.0
    0.5
    0.0
    -0.5
    -1.0

    -1.0
    -0.5
    0.0
    0.5
    1.0
    """
  end

  defp circle() do
    """
    
    """
  end

  defp point(samples) do
    samples
    |> Enum.map(fn sample ->
      color =
        if :math.sqrt(sample.x * sample.x + sample.y * sample.y) > 1 do
          "#ff0000"
        else
          "#0000ff"
        end

      """
      
      """
    end)
    |> Enum.join("\n")
  end
end
Canvas.render(samples)

Path of Approximation

alias VegaLite, as: Vl
correct = [
  %{"legend" => "correct", "pi" => :math.pi(), "count" => 1},
  %{"legend" => "correct", "pi" => :math.pi(), "count" => step_count}
]
Vl.new(width: 400, height: 300)
|> Vl.data_from_values(correct ++ approximation)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "count", type: :quantitative, title: "Step count")
|> Vl.encode_field(:y, "pi", type: :quantitative, title: "Value")
|> Vl.encode_field(:color, "legend", type: :nominal, title: "Legend")

Final Words

In this example, the full unit square is used as the space the randomly positioned points are placed in. We could have restricted this further to the first quadrant.