Skip to content

Run-and-tumble

Run-and-tumble agents in a periodic square, which diffuse with v^2 (1 + q) / (4 (1 - q)) per step.

  • Name: run_and_tumble
  • Field: physics/active_matter
  • Formalism: agent-based model
  • References: Berg 2004; Bechinger et al. 2016

Factory

@register_model(
    field='physics/active_matter',
    tags=('active matter', 'agents', 'bacteria', 'self-propulsion'),
    references=('Berg 2004', 'Bechinger et al. 2016'),
    defaults={'until': 100, 'dt': 1},
)
def run_and_tumble(
    n_agents: int = 100,
    *,
    speed: float = 0.1,
    tumble_rate: float = 0.1,
    size: float = 10.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return run-and-tumble agents in a periodic square, which diffuse with `v^2 (1 + q) / (4 (1 - q))` per step.

    The persistence `q = exp(-tumble_rate)` is the probability that an agent keeps its direction for one step.

    Args:
        n_agents:
            The number of agents.

        speed:
            The distance run per step.

        tumble_rate:
            The rate of tumbles per step.

        size:
            The side of the periodic square.

        random_state:
            The seed or generator of the initial positions and directions.

    Returns:
        system:
            The model, with the agent group `bacteria`.

    Examples:
        >>> from skcomplex.models.physics.active_matter import run_and_tumble
        >>> run_and_tumble(10).elements[0].check().prepare().size_
        10
    """
    positions, angles = _swimmers(n_agents, size, random_state)
    group = GroupElement(
        'bacteria',
        element=TumblerElement,
        size=n_agents,
        values={'theta': angles, 'speed': speed, 'tumble_rate': tumble_rate, 'width': size},
        location=positions,
    )
    return System(elements=[group], space=ContinuousSpace(extent=(size, size), boundary='periodic'))

Components

class TumblerElement(Element):
    """Run-and-tumble agent in a periodic square, which runs straight and tumbles to a random direction.

    Every step the agent moves `speed` along its direction, then tumbles with probability `1 - exp(-tumble_rate)`
    to a direction drawn uniformly, as bacteria such as E. coli do.

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

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

        theta:
            The initial direction.

        speed:
            The distance run per step.

        tumble_rate:
            The rate of tumbles per step.

        width:
            The side of the periodic square.

    Examples:
        >>> from skcomplex.models.physics.active_matter import TumblerElement
        >>> TumblerElement('bacterium').speed
        0.1
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'theta'}),
        *,
        theta: Annotated[float, StateSpec(role='direction')] = 0.0,
        speed: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        tumble_rate: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        width: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.theta = theta
        self.speed = speed
        self.tumble_rate = tumble_rate
        self.width = width

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the run-and-tumble rule.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.run_and_tumble)]

    @classmethod
    def _get_view(cls) -> type:
        """Return the class of the views of the element's agents."""
        return TumblerView

    def run_and_tumble(self, agent: TumblerView, model: ModelView) -> None:
        """Run one step along the direction, then tumble at random.

        Args:
            agent:
                The agent.

            model:
                The model.
        """
        position = np.asarray(agent.location, dtype=float)
        heading = np.array([math.cos(agent.theta), math.sin(agent.theta)])
        agent.move_to(np.mod(position + agent.speed * heading, agent.width))
        if model.rng.random() < -math.expm1(-agent.tumble_rate):
            agent.theta = float(model.rng.uniform(0.0, 2 * math.pi))