Skip to content

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)))])