Fully connected branching and annihilation
The fully connected branching and annihilation process, active above kappa = nu until it dies out.
- Name:
absorbing_birth_death - Field:
physics/statistical_mechanics - Formalism: jump process
- References: Bertin 2021; Hinrichsen 2000
Factory
@register_model(
field='physics/statistical_mechanics',
tags=('absorbing state', 'phase transition', 'birth-death process', 'metastability', 'jump process'),
references=('Bertin 2021', 'Hinrichsen 2000'),
defaults={'until': 100.0, 'dt': 0.1},
)
def absorbing_birth_death(
*,
kappa: float = 2.0,
nu: float = 1.0,
lam: float = 1.0,
volume: float = 100.0,
density: float = 1.0,
epsilon: float = 0.0,
) -> System:
"""Return the fully connected branching and annihilation process, active above `kappa = nu` until it dies out.
The deterministic density obeys `d rho / dt = (kappa - nu) rho - lambda rho^2`, with the active fixed point
`rho_0 = (kappa - nu) / lambda`. With fluctuations the empty state absorbs every run, but above the transition the
active state is metastable, with relative fluctuations `sigma / rho_0 = (lambda / (kappa - nu)) sqrt(kappa /
(lambda V))` and a lifetime that grows like `exp(V phi(0)) / nu`. Below the transition the particles die out in a
time of order `1 / (nu - kappa)`. A small `epsilon` makes the empty state leave, as in exercise 3.3 of Bertin.
Args:
kappa:
The branching rate.
nu:
The decay rate.
lam:
The annihilation rate.
volume:
The volume `V`.
density:
The initial density, rounded to a whole number of particles.
epsilon:
The rate at which a particle appears in the empty state.
Returns:
system:
The model, with the number of particles `particles.n`.
Examples:
>>> from skcomplex.models.physics.statistical_mechanics import absorbing_birth_death
>>> absorbing_birth_death(volume=50.0).elements[0].n
50.0
"""
particles = ReactingParticlesElement(
'particles',
n=float(round(density * volume)),
kappa=kappa,
nu=nu,
lam=lam,
volume=volume,
epsilon=epsilon,
)
return System(elements=[particles])
Components
class ReactingParticlesElement(Element):
"""Number `n` of particles in a volume `V`, with the reactions `A -> 2A`, `A -> 0`, and `2A -> A`.
A particle branches at rate `kappa`, decays at rate `nu`, and every pair annihilates at rate `lambda / V`, so
`W(n + 1 | n) = kappa n + epsilon delta_n0` and `W(n - 1 | n) = nu n + lambda n (n - 1) / V`. The empty state
absorbs when `epsilon = 0`.
Args:
label:
The label of the particles.
visual:
How the element is drawn, or `None` for `default_visual_`.
n:
The initial number of particles.
kappa:
The branching rate.
nu:
The decay rate.
lam:
The annihilation rate.
volume:
The volume `V`.
epsilon:
The rate at which a particle appears in the empty state.
Examples:
>>> from skcomplex.models.physics.statistical_mechanics import ReactingParticlesElement
>>> ReactingParticlesElement('particles').kappa
2.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
n: Annotated[float, StateSpec(domain=NN)] = 100.0,
kappa: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
nu: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
lam: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
volume: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 100.0,
epsilon: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.n = n
self.kappa = kappa
self.nu = nu
self.lam = lam
self.volume = volume
self.epsilon = epsilon
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the creation and the removal of one particle.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
n = s.n
return [
JumpLaw(s.kappa * n + s.epsilon * where(n < 1, 1, 0), {n: 1}),
JumpLaw(s.nu * n + s.lam * n * (n - 1) / s.volume, {n: -1}),
]