{ "cells": [ { "metadata": {}, "cell_type": "markdown", "source": [ "# Agent Activation\n", "### The Boltzmann Wealth Model" ], "id": "82ea4a268c8f43da" }, { "metadata": {}, "cell_type": "markdown", "source": [ "**Important:**\n", "- If you are just exploring Mesa and want the fastest way to execute the code we recommend executing this tutorial online in a Colab notebook. [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mesa/mesa/blob/main/docs/tutorials/2_agent_activation.ipynb) or if you do not have a Google account you can use [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/mesa/mesa/main?labpath=docs%2Ftutorials%2F2_agent_activation.ipynb) (This can take 30 seconds to 5 minutes to load)\n", "- If you are running locally, please ensure you have the latest Mesa version installed.\n", "## Tutorial Description\n", "In the [previous tutorial](1_agentset.ipynb)\n", "you learned how to query, filter, and group agents using AgentSet. Now we'll cover how\n", "to make agents actually **do** things — and why the *order* and *pattern* of activation\n", "matters.\n", "By the end of this tutorial you will know how to:\n", "- Activate agents sequentially (`do`) and in random order (`shuffle_do`)\n", "- Collect return values with `map`\n", "- Combine `select` with activation for conditional execution\n", "- Implement common activation patterns: simultaneous, staged, and type-based\n", "- Understand why activation order affects model outcomes" ], "id": "61e6f83b3c628aaa" }, { "metadata": {}, "cell_type": "markdown", "source": "### IN COLAB? - Run the next cell", "id": "4d14a4f3fd6211b3" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": "# %pip install --quiet mesa[rec]", "id": "8f073e30e48d1fe7" }, { "metadata": {}, "cell_type": "markdown", "source": "### Import Dependencies", "id": "87454e623ecbaacb" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "import numpy as np\n", "import seaborn as sns\n", "\n", "import mesa" ], "id": "80df3c310294d1cf" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## `do` and `shuffle_do`: The Core Activation Methods\n", "AgentSet provides two primary methods for activating agents:\n", "- **`do(method)`** — Calls the named method on each agent, in the current order of the set.\n", "- **`shuffle_do(method)`** — Randomly reorders agents, then calls the method on each.\n", "Both accept a method name (string) or a callable. Additional arguments are passed\n", "through to each agent's method.\n", "Let's see these in action with a minimal model." ], "id": "ea6c355f0664f0f9" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "class MoneyAgent(mesa.Agent):\n", " \"\"\"An agent with fixed initial wealth.\"\"\"\n", "\n", " def __init__(self, model):\n", " super().__init__(model)\n", " self.wealth = 1\n", "\n", " def exchange(self):\n", " if self.wealth > 0:\n", " other = self.random.choice(self.model.agents)\n", " other.wealth += 1\n", " self.wealth -= 1\n", "\n", "\n", "class MoneyModel(mesa.Model):\n", " \"\"\"A model with some number of agents.\"\"\"\n", "\n", " def __init__(self, n=10):\n", " super().__init__()\n", " MoneyAgent.create_agents(model=self, n=n)\n", "\n", " def step(self):\n", " # Random activation — each agent acts in a random order\n", " self.agents.shuffle_do(\"exchange\")" ], "id": "b1133e5d5bb80a87" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "model = MoneyModel(10)\n", "model.run_for(30)\n", "\n", "wealth = model.agents.get(\"wealth\")\n", "print(f\"Wealth distribution: {sorted(wealth, reverse=True)}\")\n", "print(f\"Total wealth: {sum(wealth)} (should be {len(model.agents)})\")" ], "id": "b0af65dbbe1f1614" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Why Activation Order Matters\n", "The order in which agents act can significantly affect model outcomes. Consider a\n", "simple scenario: if Agent A gives money to Agent B, and then Agent B gives money away,\n", "Agent B now has more to give. If the order were reversed — B acts first, then A gives\n", "to B — the outcome differs.\n", "This is a well-studied phenomenon in agent-based modeling. Comer (2014) showed that\n", "activation order can materially impact emergent behavior.\n", "Let's demonstrate this by comparing `do` (fixed order) with `shuffle_do` (random order)." ], "id": "4401f438f4047ecf" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "# Fixed order — same agent always goes first\n", "class FixedOrderModel(mesa.Model):\n", " def __init__(self, n=10):\n", " super().__init__()\n", " MoneyAgent.create_agents(model=self, n=n)\n", "\n", " def step(self):\n", " self.agents.do(\"exchange\") # Same order every step\n", "\n", "\n", "# Random order — different every step\n", "class RandomOrderModel(mesa.Model):\n", " def __init__(self, n=10):\n", " super().__init__()\n", " MoneyAgent.create_agents(model=self, n=n)\n", "\n", " def step(self):\n", " self.agents.shuffle_do(\"exchange\")\n", "\n", "\n", "# Run both multiple times and compare Gini coefficients\n", "def gini(model):\n", " x = sorted(model.agents.get(\"wealth\"))\n", " n = len(x)\n", " B = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x))\n", " return 1 + (1 / n) - 2 * B\n", "\n", "\n", "fixed_ginis = []\n", "random_ginis = []\n", "for _ in range(50):\n", " m = FixedOrderModel(50)\n", " m.run_for(100)\n", " fixed_ginis.append(gini(m))\n", "\n", " m = RandomOrderModel(50)\n", " m.run_for(100)\n", " random_ginis.append(gini(m))\n", "\n", "print(\n", " f\"Fixed order — mean Gini: {np.mean(fixed_ginis):.3f} (std: {np.std(fixed_ginis):.3f})\"\n", ")\n", "print(\n", " f\"Random order — mean Gini: {np.mean(random_ginis):.3f} (std: {np.std(random_ginis):.3f})\"\n", ")" ], "id": "8469046852670e3f" }, { "metadata": {}, "cell_type": "markdown", "source": [ "**General guidance:** Use `shuffle_do` unless your model specifically requires a fixed\n", "activation order. Random activation avoids systematic biases where early-acting agents\n", "have an inherent advantage." ], "id": "b57f413f5635a8ac" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Using Callables with `do`\n", "Instead of passing a method name as a string, you can pass a callable function directly.\n", "The function receives each agent as its first argument:" ], "id": "2c5a6aff02e46221" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "# Using a callable instead of a method name\n", "def tax_agent(agent):\n", " \"\"\"Take 10% tax from agents with wealth > 5.\"\"\"\n", " if agent.wealth > 5:\n", " tax = agent.wealth // 10\n", " agent.wealth -= tax\n", "\n", "\n", "model = MoneyModel(50)\n", "model.run_for(100)\n", "\n", "print(f\"Max wealth before tax: {model.agents.agg('wealth', max)}\")\n", "model.agents.do(tax_agent)\n", "print(f\"Max wealth after tax: {model.agents.agg('wealth', max)}\")" ], "id": "f9a93ba4f76dd9d0" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Collecting Results with `map`\n", "While `do` calls a method and discards the return values, `map` calls a method and\n", "**returns the results** as a list. Use `map` when each agent computes something you\n", "need to collect." ], "id": "770ec5c831908b7a" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "class ReportingAgent(mesa.Agent):\n", " def __init__(self, model):\n", " super().__init__(model)\n", " self.wealth = 1\n", " self.age = self.random.randint(18, 80)\n", "\n", " def report_status(self):\n", " return {\"id\": self.unique_id, \"wealth\": self.wealth, \"age\": self.age}\n", "\n", " def exchange(self):\n", " if self.wealth > 0:\n", " other = self.random.choice(self.model.agents)\n", " other.wealth += 1\n", " self.wealth -= 1\n", "\n", "\n", "class ReportingModel(mesa.Model):\n", " def __init__(self, n=10):\n", " super().__init__()\n", " ReportingAgent.create_agents(model=self, n=n)\n", "\n", " def step(self):\n", " self.agents.shuffle_do(\"exchange\")\n", "\n", "\n", "model = ReportingModel(10)\n", "model.run_for(20)\n", "\n", "# Collect status reports from all agents\n", "reports = model.agents.map(\"report_status\")\n", "print(\"Agent reports:\")\n", "for r in reports[:5]:\n", " print(f\" {r}\")" ], "id": "290f09864597e2cb" }, { "metadata": {}, "cell_type": "markdown", "source": "You can also use `map` with a callable:", "id": "d8fc2d1d0053f4c7" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "# Calculate each agent's wealth-to-age ratio\n", "ratios = model.agents.map(lambda a: a.wealth / a.age)\n", "print(f\"Wealth/age ratios (first 5): {[f'{r:.3f}' for r in ratios[:5]]}\")" ], "id": "ad6b1f0dd37c1ce3" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Conditional Activation with `select` + `do`\n", "One of the most powerful patterns in Mesa is combining `select` with activation.\n", "By filtering agents first, you can activate only those that meet specific criteria.\n", "In many real-world models, not all agents act every step. Maybe only agents with\n", "sufficient energy can move, only living agents can reproduce, or only wealthy agents\n", "pay taxes." ], "id": "ecb1004766e1fb22" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "class MoneyAgent(mesa.Agent):\n", " def __init__(self, model):\n", " super().__init__(model)\n", " self.wealth = 1\n", "\n", " def exchange(self):\n", " if self.wealth > 0:\n", " other = self.random.choice(self.model.agents)\n", " other.wealth += 1\n", " self.wealth -= 1\n", "\n", " def donate(self, recipients):\n", " \"\"\"Give 1 unit to a random recipient.\"\"\"\n", " if self.wealth > 0 and len(recipients) > 0:\n", " recipient = self.random.choice(recipients)\n", " recipient.wealth += 1\n", " self.wealth -= 1\n", "\n", "\n", "class PolicyModel(mesa.Model):\n", " \"\"\"A model where only rich agents donate to poor agents.\"\"\"\n", "\n", " def __init__(self, n=100):\n", " super().__init__()\n", " MoneyAgent.create_agents(model=self, n=n)\n", "\n", " def step(self):\n", " # First: normal exchanges\n", " self.agents.shuffle_do(\"exchange\")\n", "\n", " # Then: redistribution policy — rich donate to poor\n", " rich = self.agents.select(lambda a: a.wealth >= 5)\n", " poor = self.agents.select(lambda a: a.wealth == 0)\n", " if len(rich) > 0 and len(poor) > 0:\n", " rich.shuffle_do(\"donate\", poor)\n", "\n", "\n", "model = PolicyModel(100)\n", "model.run_for(100)\n", "\n", "broke = len(model.agents.select(lambda a: a.wealth == 0))\n", "rich = len(model.agents.select(lambda a: a.wealth >= 5))\n", "print(f\"After redistribution policy: {broke} broke agents, {rich} rich agents\")" ], "id": "f4027e504055f56f" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Common Activation Patterns\n", "Before Mesa 3.0, activation patterns were hard-coded into \"scheduler\" classes\n", "(RandomActivation, SimultaneousActivation, etc.). Now, you compose them directly\n", "from AgentSet methods. This is more flexible — you can mix and match any combination.\n", "Here are the most common patterns:" ], "id": "38580cafe7e84402" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Sequential Activation\n", "Agents act in a fixed order. The simplest pattern, but can introduce systematic bias.\n", "```python\n", "self.agents.do(\"step\")\n", "```" ], "id": "3849a6cef449a35f" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Random Activation\n", "Agents act in a new random order each step. The most common default.\n", "```python\n", "self.agents.shuffle_do(\"step\")\n", "```" ], "id": "9cb0e5bd9cc6611f" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Simultaneous Activation\n", "All agents first compute their next state, then all advance at once. This prevents\n", "early-acting agents from influencing later ones within the same step. Classic examples\n", "include Conway's Game of Life and Schelling's segregation model.\n", "```python\n", "self.agents.do(\"compute_next_state\")\n", "self.agents.do(\"advance\")\n", "```" ], "id": "9d714b843617b907" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Staged Activation\n", "Agents perform multiple actions per step, in a defined sequence of stages. For example,\n", "agents might first move, then eat, then reproduce.\n", "```python\n", "for stage in [\"move\", \"eat\", \"reproduce\"]:\n", " self.agents.shuffle_do(stage)\n", "```" ], "id": "63bc53c2cf959e74" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Type-Based Activation\n", "In models with multiple agent types, you often want each type to act separately.\n", "For example, predators and prey might take turns. Use `agents_by_type` to access\n", "the AgentSet for each type:\n", "```python\n", "for agent_type in self.agent_types:\n", " self.agents_by_type[agent_type].shuffle_do(\"step\")\n", "```\n", "Or target specific types:\n", "```python\n", "self.agents_by_type[Prey].shuffle_do(\"step\")\n", "self.agents_by_type[Predator].shuffle_do(\"step\")\n", "```" ], "id": "6df46c2b407dfa32" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Combining Patterns\n", "The real power is in combining these patterns freely. Here's an example that uses\n", "staged, type-based, and conditional activation all at once:" ], "id": "11c498de1661f5cc" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "class Prey(mesa.Agent):\n", " def __init__(self, model):\n", " super().__init__(model)\n", " self.energy = 5\n", "\n", " def move(self):\n", " self.energy -= 1\n", "\n", " def eat(self):\n", " self.energy += self.random.randint(0, 2)\n", "\n", " def reproduce(self):\n", " if self.energy > 8:\n", " self.energy -= 4\n", " Prey(self.model) # New prey is automatically registered\n", "\n", "\n", "class Predator(mesa.Agent):\n", " def __init__(self, model):\n", " super().__init__(model)\n", " self.energy = 10\n", " self.kills = 0\n", "\n", " def move(self):\n", " self.energy -= 2 # Predators use more energy\n", "\n", " def hunt(self):\n", " prey_agents = self.model.agents_by_type.get(Prey)\n", " if prey_agents and len(prey_agents) > 0 and self.energy > 0:\n", " target = self.random.choice(prey_agents)\n", " target.remove()\n", " self.energy += 5\n", " self.kills += 1\n", "\n", "\n", "class EcosystemModel(mesa.Model):\n", " def __init__(self, n_prey=50, n_predators=5):\n", " super().__init__()\n", " Prey.create_agents(model=self, n=n_prey)\n", " Predator.create_agents(model=self, n=n_predators)\n", "\n", " def step(self):\n", " # Stage 1: All agents move (random order within each type)\n", " for agent_type in self.agent_types:\n", " self.agents_by_type[agent_type].shuffle_do(\"move\")\n", "\n", " # Stage 2: Type-specific actions\n", " if Prey in self.agents_by_type:\n", " self.agents_by_type[Prey].shuffle_do(\"eat\")\n", " if Predator in self.agents_by_type:\n", " self.agents_by_type[Predator].shuffle_do(\"hunt\")\n", "\n", " # Stage 3: Only prey with enough energy reproduce\n", " if Prey in self.agents_by_type:\n", " fertile = self.agents_by_type[Prey].select(lambda a: a.energy > 8)\n", " fertile.do(\"reproduce\")\n", "\n", " # Remove dead agents (energy depleted)\n", " dead = self.agents.select(lambda a: a.energy <= 0)\n", " for agent in dead:\n", " agent.remove()\n", "\n", "\n", "eco = EcosystemModel(50, 5)\n", "eco.run_for(20)\n", "\n", "n_prey = len(eco.agents_by_type.get(Prey, []))\n", "n_pred = len(eco.agents_by_type.get(Predator, []))\n", "print(f\"After 20 steps: {n_prey} prey, {n_pred} predators, {len(eco.agents)} total\")" ], "id": "8bdc3d343c52bced" }, { "metadata": {}, "cell_type": "markdown", "source": [ "This example demonstrates several key points:\n", "- **Staged activation**: All agents move first, then type-specific actions happen\n", "- **Type-based activation**: Prey eat while predators hunt\n", "- **Conditional activation**: Only fertile prey reproduce\n", "- **Dynamic agent creation/removal**: Prey reproduce and dead agents are removed\n", "None of this required any special scheduler class — just `do`, `shuffle_do`, and\n", "`select`, composed in the order that makes sense for your model." ], "id": "c2a9cb1c471aacd7" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Summary\n", "| Method | Purpose | Returns |\n", "|---|---|---|\n", "| `do(method)` | Call method on each agent (fixed order) | The AgentSet |\n", "| `shuffle_do(method)` | Call method on each agent (random order) | The AgentSet |\n", "| `map(method)` | Call method on each agent and collect results | List of results |\n", "**Key activation patterns:**\n", "- **Random**: `agents.shuffle_do(\"step\")` — use this as your default\n", "- **Sequential**: `agents.do(\"step\")` — when order is intentional\n", "- **Simultaneous**: `agents.do(\"compute\")` then `agents.do(\"advance\")`\n", "- **Staged**: loop over stages, calling `do`/`shuffle_do` for each\n", "- **Type-based**: use `agents_by_type[Type].shuffle_do(\"step\")`\n", "- **Conditional**: `agents.select(condition).do(\"step\")`\n", "Combine these freely to express exactly the activation logic your model needs." ], "id": "6ce65978186db4bc" }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Next Steps\n", "Check out the [Event Scheduling & Time tutorial](3_event_scheduling.ipynb)\n", "to learn how to schedule events at specific times, create recurring events, and control\n", "how your simulation progresses through time." ], "id": "fefdd2721e22c3c3" }, { "cell_type": "markdown", "id": "c2c39497e4eb19ab", "metadata": {}, "source": "[Comer2014] Comer, Kenneth W. \"Who Goes First? An Examination of the Impact of Activation on Outcome Behavior in AgentBased Models.\" George Mason University, 2014. http://mars.gmu.edu/bitstream/handle/1920/9070/Comer_gmu_0883E_10539.pdf\n" } ], "metadata": {}, "nbformat": 4, "nbformat_minor": 5 }