Diffusion-limited aggregation
Diffusion-limited aggregation on a periodic grid, where random walkers stick to a seed at the center.
- Name:
diffusion_limited_aggregation - Field:
physics/statistical_mechanics - Formalism: agent-based model
- References: Witten and Sander 1981; Charbonneau 2017
Factory
@register_model(
field='physics/statistical_mechanics',
tags=('agents', 'aggregation', 'fractal', 'random walk'),
references=('Witten and Sander 1981', 'Charbonneau 2017'),
defaults={'until': 200, 'dt': 1},
)
def diffusion_limited_aggregation(
n_walkers: int = 400,
*,
size: int = 41,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return diffusion-limited aggregation on a periodic grid, where random walkers stick to a seed at the center.
Walkers that touch the aggregate join it, so the tips of the aggregate catch most walkers and it grows into a
branched fractal of dimension about 1.7.
Args:
n_walkers:
The number of walkers, the seed included.
size:
The side of the grid.
random_state:
The seed or generator of the initial cells of the walkers.
Returns:
system:
The model, with the agent group `walkers`.
Examples:
>>> from skcomplex.models.physics.statistical_mechanics import diffusion_limited_aggregation
>>> diffusion_limited_aggregation(10, size=11).elements[0].check().prepare().size_
10
"""
rng = np.random.default_rng(random_state)
middle = size // 2
cells = rng.integers(0, size, (n_walkers, 2))
cells[0] = middle
stuck = np.zeros(n_walkers)
stuck[0] = 1.0
walkers = GroupElement(
'walkers',
element=WalkerElement,
size=n_walkers,
values={'stuck': stuck},
location=[tuple(map(int, cell)) for cell in cells],
)
return System(
elements=[walkers],
space=LatticeSpace((size, size), boundary='periodic', neighborhood='von_neumann'),
)
Components
class WalkerElement(Element):
"""Random walker of diffusion-limited aggregation, which sticks when it reaches a stuck walker.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
stuck:
Whether the walker belongs to the aggregate.
Examples:
>>> from skcomplex.models.physics.statistical_mechanics import WalkerElement
>>> WalkerElement('walker').stuck
0.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'stuck', 'color_range': (0.0, 1.0)}),
*,
stuck: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.stuck = stuck
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the walk-and-stick rule.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.walk)]
@classmethod
def _get_view(cls) -> type:
"""Return the class of the views of the element's agents."""
return WalkerView
def walk(self, agent: WalkerView, model: ModelView) -> None:
"""Stick next to the aggregate, or step to a random neighbouring cell.
Args:
agent:
The walker.
model:
The model.
"""
if agent.stuck:
return
if any(isinstance(other, WalkerView) and other.stuck for other in agent.neighbors()):
agent.stuck = 1.0
return
cells = agent.neighbor_cells()
agent.move_to(cells[int(model.rng.integers(len(cells)))])