Skip to main content

Galton Board

· One min read
Kirill Vasin

Adapted example by Gabriel Lemieux (2025)

The Galton Board notebook simulates the behavior of a physical Galton board—a device that demonstrates the Central Limit Theorem using a grid of pegs and falling balls that bounce randomly to form a bell-shaped distribution.

Download original notebook
(*BB[*)(* Initialization *)(*,*)(*"1:eJxTTMoPSmNhYGAo5gcSAUX5ZZkpqSn+BSWZ+XnFaYwgCS4g4Zyfm5uaV+KUXxEMUqxsbm6exgSSBPGCSnNSg9mAjOCSosy8dLBYSFFpKpoKkDkeqYkpEFXBILO1sCgJSczMQVYCAOFrJEU="*)(*]BB*)
ClearAll[ball, pos, mom, bins, peg];

ball = <|
  r -> 0.1,         (* Radius of the ball *)
  n -> 50          (* Number of simultaneous balls *)
|>;

peg = <|
  r -> 0.25         (* Radius of the pegs *)
|>;

sim = <|
  \[CapitalDelta]t -> 0.05,    (* Higher Value need to correct collision positions *)
  \[Epsilon] -> 0.4,           (* 1.0 means all energy back in the ball *)
  g -> {0, -9.81},             (* You can tilt the board *)
  n -> 300                    (* Total Number of Balls *)
|>;

(* Initialize Position and Momentum Matrices *)
randomBall := {RandomReal[{-0.05, 0.05}], RandomReal[{0, 1}] + 10};
ball[pos] = Table[randomBall, ball[n]];
ball[mom] = Table[0, ball[n], 2];
ball[bins] = {};

(* positions of bin rectangles : only for visualizaiton *)
rectangleBins = Table[0.01, {i,-8,8,1}];

(* Peg position *)
peg[pos] = Flatten[Table[{ii + 0.5 jj, jj}, {ii, -15, 10}, {jj, 0, 8}], 1];

visPositions = ball[pos];

simulateNext[] := With[{},
  (* Step *)
  (* Scheme: Backward Euler *)

  (* Next Step Momentum *)
  ball[mom] += Threaded[sim[\[CapitalDelta]t] sim[g]];

  (* Next Position Prediction *)
  next = ball[pos] + sim[\[CapitalDelta]t] ball[mom];

  (* Distance: Do not parallelize, it's already in the algo. *)
  dist = DistanceMatrix[next, peg[pos]];

  (* Collisions between current and future step *)
  col = MapIndexed[
    {v, i} |-> {
      SelectFirst[v, (# < ball[r] + peg[r]) & -> "Index"], i[[1]]
    } /. ({Missing[_], _} -> Nothing),
    dist
  ];

  (* Correct Momentum to include collisions *)
  If[Length[col] > 1,
    {
      up, val
    } = Transpose[
       MapApply[
        {p, b} |-> {
          b,
          sim[\[Epsilon]] * Norm[ball[mom][[b]]] Normalize[next[[b]] - peg[pos][[p]]]
        },
        col
      ]
    ];
    momt = ball[mom];
    momt[[up]] = val;
    ball[mom] = momt;
  ];

  (* Update pos with corrected momentum *)
  ball[pos] += sim[\[CapitalDelta]t] ball[mom];

  (* Filter through the balls, remove/reset and register those exiting *)
  exited = Select[ball[pos], #[[2]] < 0 & -> "Index"];
  ingame = Select[ball[pos], #[[2]] > 0 & -> "Index"];
  
  If[Length[exited] > 0,
    ball[bins] = Join[ball[bins], ball[pos][[exited, 1]]]
  ];

  (* Remove them from the pos and mom matrices *) 
  ball[pos] = ball[pos][[ingame]];
  ball[mom] = ball[mom][[ingame]];

  (* Insert new ball *)
  left = sim[n] - Length[ball[bins]] - Length[ball[pos]];
  toAdd = Min[left, ball[n] - Length[ball[pos]]];
  Do[
    AppendTo[ball[pos], randomBall];
    AppendTo[ball[mom], {0, 0}],
    toAdd
  ];

  (* Update visible positions *)
  visPositions = ball[pos];

  (* Update bins *)
  If[Length[ball[bins]] > 5,  With[{h = HistogramList[ball[bins], {-8, 8, 1}]},
    rectangleBins = Rescale @ ReplacePart[
        ConstantArray[0.01, Length@h[[1]]],           (* 17 entries at .01 *)
        Thread[Most@h[[1]] + 9 -> h[[2]]]              (* now integers 1,2,…16 *)
      ]
  ]];
];

Display function:

Graphics[{

  {Orange, Table[With[{i=i, j=i+9},
    Rectangle[{i,0}, {i+1, 2.0 rectangleBins[[j]] // Offload}]
  ], {i,-8,8,1}]},

  {LightGray, EdgeForm[Gray], Disk[{#1, #2}, peg[r]]} & @@@ peg[pos],
  {PointSize[0.02], Opacity[0.8], Blue, Point[visPositions // Offload]},
  
  AnimationFrameListener[visPositions // Offload, "Event"->"frameEvent"]
},
  AspectRatio -> 0.5,
  PlotRange -> {10 {-1, 1}, 10 {0, 1}},
  PlotRangeClipping -> True,
  ImageSize -> Large,
  TransitionType->None
]

EventHandler["frameEvent", Function[Null,
  If[Length[ball[pos]] > 0, simulateNext[] ];
]];
(*VB[*)(FrontEndRef["5923fdb1-4965-4c18-880e-848cf2ab7064"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKm1oaGaelJBnqmliameqaJBta6FpYGKTqWphYJKcZJSaZG5iZAAB5/RUj"*)(*]VB*)

To reset the model run:

randomBall := {RandomReal[{-0.05, 0.05}], RandomReal[{0, 1}] + 10};
ball[pos] = Table[randomBall, ball[n]];
ball[mom] = Table[0, ball[n], 2];
ball[bins] = {};
visPositions = ball[pos];

rectangleBins = Table[0.01, {i,-8,8,1}];