Skip to content
The Lumenary

Math laboratory

Percolation crossings and the Erdos-Renyi giant component

Monte Carlo estimates of left-right crossing probabilities for bond percolation on self-dual (L+1) x L rectangles (L = 8, 24) for p from 0.35 to 0.65, which sharpen around Kesten's p_c = 1/2, and of the largest component of random graphs with n = 2000 vertices and average degree c, compared with the branching-process prediction beta = 1 - exp(-c beta). Statistical evidence from small samples, not proof.

Back to the labStatistical evidenceExperimental evidence
Percolation crossings and the Erdos-Renyi giant componentPythonStatistical evidenceExperimental evidenceComputationally tested
"""Phase transitions: bond percolation on a grid and the giant component.

Part 1. Bond percolation on a square grid: each edge is open with
probability p. We estimate by simulation the probability that open edges
connect the left side of an (L + 1) x L rectangle to its right side. As L
grows the curve sharpens into a step at p = 1/2, Kesten's critical value;
at p = 1/2 the crossing probability is exactly 1/2 for every L, because the
rectangle is chosen to be self-dual.

Part 2. The Erdos-Renyi random graph with n vertices and m = c n / 2 random
edges (asymptotically the same as G(n, c/n)). The largest component has a
vanishing fraction of the vertices when c < 1 and a fraction close to the
positive root beta of beta = 1 - exp(-c beta) when c > 1.

Evidence: statistical (Monte Carlo with a fixed seed, small sizes and few
trials, with approximate standard errors). Simulations near a critical
point estimate behaviour; the exact critical values are theorems.
"""

import math
import random

def find(parent, x):
    """Root of x in a union-find forest, with path halving."""
    while parent[x] != x:
        parent[x] = parent[parent[x]]
        x = parent[x]
    return x

def union(parent, size, a, b):
    """Merge the components of a and b (union by size)."""
    ra, rb = find(parent, a), find(parent, b)
    if ra == rb:
        return
    if size[ra] < size[rb]:
        ra, rb = rb, ra
    parent[rb] = ra
    size[ra] += size[rb]

def crosses(n, p, rng):
    """Is there an open left-right crossing of the self-dual rectangle R_n?

    R_n has n interior columns x = 1..n and n + 1 rows y = 0..n. Each row
    has n + 1 horizontal edges, from the left side (x = 0) through the
    interior to the right side (x = n + 1), and each interior column has n
    vertical edges. Its planar dual is a rotated copy of R_n, so at p = 1/2
    a crossing occurs with probability exactly 1/2.
    """
    count = n * (n + 1) + 2
    parent, size = list(range(count)), [1] * count
    left, right = count - 2, count - 1

    def node(x, y):
        return y * n + (x - 1)

    for y in range(n + 1):
        if rng.random() < p:
            union(parent, size, left, node(1, y))
        if rng.random() < p:
            union(parent, size, node(n, y), right)
        for x in range(1, n):
            if rng.random() < p:
                union(parent, size, node(x, y), node(x + 1, y))
    for x in range(1, n + 1):
        for y in range(n):
            if rng.random() < p:
                union(parent, size, node(x, y), node(x, y + 1))
    return find(parent, left) == find(parent, right)

def crossing_probability(n, p, trials, rng):
    """Fraction of trials with an open left-right crossing."""
    return sum(crosses(n, p, rng) for _ in range(trials)) / trials

def largest_component_fraction(n, c, rng):
    """Largest component of a random graph with n vertices and c n / 2 edges."""
    parent, size = list(range(n)), [1] * n
    for _ in range(round(c * n / 2)):
        a, b = rng.randrange(n), rng.randrange(n)
        if a != b:
            union(parent, size, a, b)
    return max(size[find(parent, v)] for v in range(n)) / n

def giant_fraction(c, iterations=200):
    """Largest root of beta = 1 - exp(-c beta) in [0, 1].

    For c <= 1 the only root is 0 (proved in the lesson's Lean file); for
    c > 1 iterating beta -> 1 - exp(-c beta) from beta = 1 converges to the
    positive root.
    """
    if c <= 1:
        return 0.0
    beta = 1.0
    for _ in range(iterations):
        beta = 1 - math.exp(-c * beta)
    return beta

def main():
    rng = random.Random(1957)
    ps = [0.35, 0.45, 0.5, 0.55, 0.65]
    trials = 200
    print(f"Part 1: P(left-right open crossing), {trials} trials per entry")
    print("  p    " + "".join(f"{p:>8}" for p in ps))
    for n in [8, 24]:
        est = [crossing_probability(n, p, trials, rng) for p in ps]
        print(f"  L = {n:<3}" + "".join(f"{e:>8.2f}" for e in est))
    print(f"  (standard error of each entry at most about "
          f"{0.5 / math.sqrt(trials):.2f})")
    print()
    n, reps = 2000, 5
    print(f"Part 2: random graph, n = {n} vertices, average degree c "
          f"({reps} samples)")
    print(f"  {'c':>4}{'largest component / n':>24}{'beta(c) theory':>17}")
    for c in [0.5, 0.9, 1.5, 2.0, 3.0]:
        sim = sum(largest_component_fraction(n, c, rng)
                  for _ in range(reps)) / reps
        print(f"  {c:>4}{sim:>24.3f}{giant_fraction(c):>17.3f}")
    print("  (for c < 1 the largest component has O(log n) vertices,")
    print("  so its fraction tends to 0 only slowly as n grows)")
    print()
    print("Epistemic status: Monte Carlo estimates from a few hundred random")
    print("samples on small systems (statistical evidence). They are")
    print("consistent with Kesten's theorem p_c = 1/2 and the Erdos-Renyi")
    print("theorem, but cannot determine critical values or exponents exactly.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in