Skip to content

Zero-range process

The zero-range process on a ring, which condenses above the critical density 1 / (b - 2) for b > 2.

  • Name: zero_range_process
  • Field: physics/statistical_mechanics
  • Formalism: jump process
  • References: Evans and Hanney 2005; Bertin 2021

Factory

@register_model(
    field='physics/statistical_mechanics',
    tags=('zero-range process', 'condensation', 'nonequilibrium', 'jump process'),
    references=('Evans and Hanney 2005', 'Bertin 2021'),
    defaults={'until': 2000.0, 'dt': 10.0, 'backend': NumbaBackend()},
)
def zero_range_process(
    n_sites: int = 50,
    *,
    density: float = 3.0,
    b: float = 4.0,
    p: float = 1.0,
    q: float = 0.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the zero-range process on a ring, which condenses above the critical density `1 / (b - 2)` for `b > 2`.

    A particle leaves a site with `n` particles at rate `u(n) = 1 + b / n`, to the right with weight `p` and to the
    left with weight `q`. The stationary distribution factorizes with the weights `f(n) = prod_k 1 / u(k)`, which
    decay as `n^-b`. Above the critical density a fluid at density `1 / (b - 2)` coexists with one site that holds the
    excess particles.

    Args:
        n_sites:
            The number of sites.

        density:
            The number of particles per site.

        b:
            The strength of the decrease of the departure rate with the occupation.

        p:
            The rate scale of hops to the right.

        q:
            The rate scale of hops to the left.

        random_state:
            The seed or generator of the initial sites of the particles.

    Returns:
        system:
            The model, with the occupations `sites.n`.

    Examples:
        >>> from skcomplex.models.physics.statistical_mechanics import zero_range_process
        >>> system = zero_range_process(10, density=2.0)
        >>> float(system.elements[0].values['n'].sum()), len(system.interactions)
        (20.0, 1)
    """
    occupations = _scatter(round(density * n_sites), n_sites, random_state)
    sites = LatticeElement('sites', element=ParticleSiteElement, space=LatticeSpace(n_sites), values={'n': occupations})
    return System(elements=[sites], interactions=[ZeroRangeHopsInteraction('hops', sites='sites', p=p, q=q, b=b)])

Components

class ParticleSiteElement(Element):
    """Site of a lattice of particles, holding the number `n` of particles or quanta on it.

    Args:
        label:
            The label of the site or the group.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        n:
            The initial number of particles.

    Examples:
        >>> from skcomplex.models.physics.statistical_mechanics import ParticleSiteElement
        >>> ParticleSiteElement('site', n=3.0).n
        3.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        n: Annotated[float, StateSpec(domain=NN)] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.n = n

class ZeroRangeHopsInteraction(TransferInteraction):
    """Hops of one particle from every site of a ring, to the right at rate `p u(n)` and to the left at rate `q u(n)`.

    The rate depends only on the occupation `n` of the departure site, through `u(n) = 1 + b / n`. It is a transfer of
    one particle along every edge of the ring, to the right and back.

    Args:
        label:
            The label of the hops.

        sites:
            The group of sites, in ring order.

        p:
            The rate scale of hops to the right.

        q:
            The rate scale of hops to the left.

        b:
            The strength of the decrease of `u(n)` with the occupation, 0 for a constant rate.

    Examples:
        >>> from skcomplex.models.physics.statistical_mechanics import ZeroRangeHopsInteraction
        >>> ZeroRangeHopsInteraction('hops', sites='sites').b
        4.0
    """

    def __init__(
        self,
        label: str,
        *,
        sites: Annotated[str, RoleSpec()],
        p: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        q: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
        b: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 4.0,
    ) -> None:
        super().__init__(
            label,
            source=sites,
            target=sites,
            state='n',
            amount=1.0,
            rate=self._get_right_rate,
            reverse_rate=self._get_left_rate,
            offset=1,
        )
        self.sites = sites
        self.p = p
        self.q = q
        self.b = b

    def _get_holders(self, namespace: SimpleNamespace) -> list[tuple[SimpleNamespace, SimpleNamespace]]:
        """Return the sites, whose every member hops to the next one and back."""
        return [(namespace.sites, namespace.sites)]

    @staticmethod
    def _get_right_rate(s: SimpleNamespace) -> Expression:
        """Return the rate `p u(n)` of a hop to the right."""
        return s.p * ZeroRangeHopInteraction._departure_rate(s.source.n, s.b)

    @staticmethod
    def _get_left_rate(s: SimpleNamespace) -> Expression:
        """Return the rate `q u(n)` of a hop to the left, from the site on the right."""
        return s.q * ZeroRangeHopInteraction._departure_rate(s.target.n, s.b)