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