Source code for physicskit.statphys.chapters.ehrenfest_urn

"""The Ehrenfest urn model: the simplest illustration of statistical irreversibility.

Loschmidt and Zermelo objected that Boltzmann's H-theorem could not be
right, since microscopically reversible dynamics cannot produce genuinely
irreversible macroscopic behavior (Loschmidt), and any bounded mechanical
system must eventually return arbitrarily close to its initial state
(Zermelo, via Poincare recurrence). The Ehrenfests' 1907 urn model resolves
both objections in the simplest possible setting: the dynamics is exactly
reversible and does recur exactly, yet for any macroscopic number of balls
the expected recurrence time is astronomically long, while the approach to
equilibrium is essentially instantaneous -- reconciling reversible
microphysics with an observed arrow of time.
"""

from __future__ import annotations

import numpy as np
from scipy.special import gammaln

__all__ = ["EhrenfestUrn"]


[docs] class EhrenfestUrn: """The Ehrenfest urn model of N labeled balls exchanged between two boxes. At each time step, one of the ``n_balls`` balls (chosen uniformly at random) is moved from whichever box it currently occupies to the other. Parameters ---------- n_balls : int, default=100 Total number of balls, N. n_left_init : int, optional Initial number of balls in the left box. Defaults to ``n_balls`` (all balls start in one box, a maximally non-equilibrium state). seed : int, optional Seed for the ball-selection sequence. Attributes ---------- n_left : int Current number of balls in the left box. """ def __init__(self, n_balls=100, n_left_init=None, seed=None): self.n_balls = n_balls self.n_left = n_balls if n_left_init is None else n_left_init self._rng = np.random.default_rng(seed)
[docs] def step(self): """Move one uniformly random ball to the other box. Returns ------- int The updated :attr:`n_left`. """ chosen = self._rng.integers(0, self.n_balls) if chosen < self.n_left: self.n_left -= 1 else: self.n_left += 1 return self.n_left
[docs] def entropy(self): """Boltzmann entropy :math:`S/k_B = \\ln W` of the current macrostate. :math:`W = \\binom{N}{n_{\\text{left}}}` counts the ball-labelings consistent with the current left-box occupancy; the binomial coefficient is evaluated via the log-gamma function (``scipy.special.gammaln``) to stay numerically stable for large :math:`N`. Returns ------- float """ N, n = self.n_balls, self.n_left return float(gammaln(N + 1) - gammaln(n + 1) - gammaln(N - n + 1))
[docs] def run(self, n_steps): """Run the urn for ``n_steps`` and record its history. Parameters ---------- n_steps : int Number of steps to simulate. Returns ------- dict of str -> ndarray Keys ``"t"``, ``"n_left"``, ``"entropy"``, each of length ``n_steps + 1`` (including the initial state). Examples -------- >>> urn = EhrenfestUrn(n_balls=50, seed=0) >>> history = urn.run(200) >>> history["n_left"].shape (201,) >>> int(history["n_left"][0]) 50 """ n_left = np.empty(n_steps + 1, dtype=np.int64) entropy = np.empty(n_steps + 1, dtype=np.float64) n_left[0] = self.n_left entropy[0] = self.entropy() for t in range(1, n_steps + 1): self.step() n_left[t] = self.n_left entropy[t] = self.entropy() return {"t": np.arange(n_steps + 1), "n_left": n_left, "entropy": entropy}