Ehrenfest urn model of two boxes
The Ehrenfest urn model, balls moved one at a time between two boxes, which relaxes to equal counts.
- Name:
ehrenfest_urn - Field:
physics/statistical_mechanics - Formalism: discrete map
- References: Ehrenfest and Ehrenfest 1907; Barsky 2020; Kac 1947
Factory
@register_model(
field='physics/statistical_mechanics',
tags=('markov chain', 'irreversibility', 'entropy', 'discrete map'),
references=('Ehrenfest and Ehrenfest 1907', 'Barsky 2020', 'Kac 1947'),
defaults={'until': 200, 'dt': 1},
)
def ehrenfest_urn(n_balls: int = 40, *, in_first: int | None = None) -> System:
"""Return the Ehrenfest urn model, balls moved one at a time between two boxes, which relaxes to equal counts.
At every step one of the `N` numbered balls is drawn at random and moved to the other box, so the count `n` of the
first box falls by one with probability `n / N` and rises by one otherwise. Started with all balls in the first
box, the count relaxes to `N / 2` with the exponential mean `N / 2 + (n_0 - N / 2) (1 - 2 / N)^t`, and then
fluctuates around it: the chain is reversible, yet the relaxation looks irreversible. Its stationary distribution
is the binomial distribution of `N` balls with probability one half.
Args:
n_balls:
The number of balls `N`.
in_first:
The initial number of balls in the first box, or `None` for all of them.
Returns:
system:
The model, with the count `urn.n` of balls in the first box.
Examples:
>>> from skcomplex.models.physics.statistical_mechanics import ehrenfest_urn
>>> ehrenfest_urn(10).elements[0].n
10
"""
n = n_balls if in_first is None else in_first
return System(elements=[EhrenfestUrnElement('urn', n=n, N=n_balls)])
Components
class EhrenfestUrnElement(Element):
"""First box of the Ehrenfest urn, which holds `n` of the `N` balls, one of which moves to the other box every step.
The count falls by one with the probability `n / N` that the drawn ball is in the box, and rises by one otherwise.
Args:
label:
The label of the box.
visual:
How the box is drawn, or `None` for `default_visual_`.
n:
The initial number of balls in the box.
N:
The number of balls in both boxes.
Attributes:
default_visual_:
The visual of the element when it sets none.
Examples:
>>> from skcomplex.models.physics.statistical_mechanics import EhrenfestUrnElement
>>> EhrenfestUrnElement('urn', n=10).N
40
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
n: Annotated[int, StateSpec(domain=NN)] = 0,
N: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 40,
) -> None:
super().__init__(label, visual=visual)
self.n = n
self.N = N
def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
"""Return the move of one ball, out of the box with probability `n / N` and into it otherwise.
Args:
namespace:
The namespace of the symbols the laws can use.
Returns:
laws:
The update of the count.
"""
s = namespace
return [UpdateLaw(s.n, s.n + 1 - 2 * bernoulli('pick', s.n / s.N))]
Equations
urn.n: n_urn(t + 1) == n_urn(t) - 2*where(bernoulli_pick_urn < n_urn(t)/N_urn, 1, 0) + 1