Agent-based modeling (ABM) in Python framework with spatial grids, agent schedulers, data collection tools, and browser-based visualization capabilities
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Mesa provides two comprehensive spatial modeling approaches: the traditional mesa.space module (in maintenance mode) and the modern mesa.discrete_space module (under active development). Both systems enable positioning agents in space and managing spatial relationships, but with different design philosophies and capabilities.
# Traditional spatial system (maintenance mode)
from mesa.space import (
MultiGrid, SingleGrid, HexSingleGrid, HexMultiGrid,
ContinuousSpace, NetworkGrid, PropertyLayer
)
# Modern discrete spatial system (active development)
from mesa.discrete_space import (
Grid, HexGrid, Network, VoronoiGrid,
OrthogonalMooreGrid, OrthogonalVonNeumannGrid,
Cell, CellAgent, FixedAgent, Grid2DMovingAgent, CellCollection,
PropertyLayer, DiscreteSpace
)
# Type definitions
from typing import Tuple, List, Union, Optional, Iterable
import numpy as np
# Spatial coordinate types
Coordinate = tuple[int, int]
FloatCoordinate = tuple[float, float] | np.ndarray
NetworkCoordinate = int
Position = Coordinate | FloatCoordinate | NetworkCoordinate
GridContent = Agent | None
MultiGridContent = list[Agent]The traditional spatial system provides well-established grid and space classes that have been the foundation of Mesa models. These classes are now in maintenance mode with new development focused on mesa.discrete_space.
Grid where each cell contains at most one agent.
from mesa.space import SingleGrid
class SingleGrid:
"""
Grid where each cell contains at most one agent.
Provides basic grid functionality with unique occupancy constraint.
"""
def __init__(self, width: int, height: int, torus: bool = False):
"""
Initialize a SingleGrid.
Parameters:
width: Grid width in cells
height: Grid height in cells
torus: Whether the grid wraps around at edges
"""
...
def place_agent(self, agent, pos: Coordinate):
"""
Place an agent at a specific position.
Parameters:
agent: Agent to place
pos: (x, y) coordinate tuple
"""
...
def move_agent(self, agent, pos: Coordinate):
"""
Move an agent to a new position.
Parameters:
agent: Agent to move
pos: New (x, y) coordinate tuple
"""
...
def remove_agent(self, agent):
"""Remove an agent from the grid."""
...
def get_cell_list_contents(self, cell_list: List[Coordinate]) -> List[Agent]:
"""Get all agents in specified cells."""
...
def get_neighbors(self, pos: Coordinate, moore: bool = True,
include_center: bool = False, radius: int = 1) -> List[Coordinate]:
"""
Get neighboring positions.
Parameters:
pos: Center position
moore: If True, use Moore neighborhood (8-connected), else Von Neumann (4-connected)
include_center: Whether to include the center position
radius: Neighborhood radius
"""
...
def get_neighborhood(self, pos: Coordinate, moore: bool = True,
include_center: bool = False, radius: int = 1) -> List[Coordinate]:
"""Get neighborhood positions (alias for get_neighbors)."""
...
def iter_neighbors(self, pos: Coordinate, moore: bool = True,
include_center: bool = False, radius: int = 1) -> Iterator[Agent]:
"""Iterate over agents in neighboring cells."""
...
def find_empty(self) -> Coordinate | None:
"""Find a random empty cell, or None if grid is full."""
...
def exists_empty_cells(self) -> bool:
"""Check if any empty cells exist."""
...Grid where each cell can contain multiple agents.
from mesa.space import MultiGrid
class MultiGrid:
"""
Grid where each cell can contain multiple agents.
Extends SingleGrid functionality to support multiple agents per cell.
"""
def __init__(self, width: int, height: int, torus: bool = False):
"""
Initialize a MultiGrid.
Parameters:
width: Grid width in cells
height: Grid height in cells
torus: Whether the grid wraps around at edges
"""
...
def place_agent(self, agent, pos: Coordinate):
"""Add an agent to a cell (multiple agents allowed)."""
...
def move_agent(self, agent, pos: Coordinate):
"""Move an agent to a new position."""
...
def remove_agent(self, agent):
"""Remove an agent from the grid."""
...
def get_cell_list_contents(self, cell_list: List[Coordinate]) -> List[Agent]:
"""Get all agents in specified cells."""
...
# Inherits all SingleGrid methods with multi-agent behaviorHexagonal grid variants providing 6-neighbor connectivity.
from mesa.space import HexSingleGrid, HexMultiGrid
class HexSingleGrid(SingleGrid):
"""Hexagonal grid with single agent per cell."""
def get_neighbors(self, pos: Coordinate, include_center: bool = False,
radius: int = 1) -> List[Coordinate]:
"""Get hexagonal neighbors (6-connected)."""
...
class HexMultiGrid(MultiGrid):
"""Hexagonal grid with multiple agents per cell."""
def get_neighbors(self, pos: Coordinate, include_center: bool = False,
radius: int = 1) -> List[Coordinate]:
"""Get hexagonal neighbors (6-connected)."""
...Two-dimensional continuous space with real-valued coordinates.
from mesa.space import ContinuousSpace
class ContinuousSpace:
"""
Two-dimensional continuous space with real-valued coordinates.
Enables positioning agents at any (x, y) coordinate within defined bounds.
"""
def __init__(self, x_max: float, y_max: float, torus: bool = False,
x_min: float = 0, y_min: float = 0):
"""
Initialize continuous space.
Parameters:
x_max: Maximum x coordinate
y_max: Maximum y coordinate
torus: Whether space wraps around at boundaries
x_min: Minimum x coordinate
y_min: Minimum y coordinate
"""
...
def place_agent(self, agent, pos: FloatCoordinate):
"""
Place an agent at continuous coordinates.
Parameters:
agent: Agent to place
pos: (x, y) coordinate tuple with float values
"""
...
def move_agent(self, agent, pos: FloatCoordinate):
"""Move an agent to new continuous coordinates."""
...
def remove_agent(self, agent):
"""Remove an agent from the space."""
...
def get_neighbors(self, pos: FloatCoordinate, radius: float,
include_center: bool = False) -> List[Agent]:
"""
Get agents within radius of position.
Parameters:
pos: Center position
radius: Search radius
include_center: Whether to include agent at exact center
"""
...
def get_heading(self, pos_1: FloatCoordinate, pos_2: FloatCoordinate) -> float:
"""Get heading from pos_1 to pos_2 in radians."""
...
def get_distance(self, pos_1: FloatCoordinate, pos_2: FloatCoordinate) -> float:
"""Get Euclidean distance between positions."""
...
def move_by(self, agent, dx: float, dy: float):
"""Move agent by relative displacement."""
...
def torus_adj(self, pos: FloatCoordinate) -> FloatCoordinate:
"""Adjust position for torus topology."""
...Network-based space where agents are positioned on graph nodes.
from mesa.space import NetworkGrid
import networkx as nx
class NetworkGrid:
"""
Network-based space where agents are positioned on nodes.
Uses NetworkX graphs to define spatial structure and connectivity.
"""
def __init__(self, G: nx.Graph):
"""
Initialize network grid.
Parameters:
G: NetworkX graph defining the network structure
"""
...
def place_agent(self, agent, node_id: NetworkCoordinate):
"""
Place agent on a network node.
Parameters:
agent: Agent to place
node_id: Network node identifier
"""
...
def move_agent(self, agent, node_id: NetworkCoordinate):
"""Move agent to a different node."""
...
def remove_agent(self, agent):
"""Remove agent from the network."""
...
def get_neighbors(self, node_id: NetworkCoordinate) -> List[NetworkCoordinate]:
"""Get neighboring nodes in the network."""
...
def get_all_cell_contents(self) -> List[Agent]:
"""Get all agents in the network."""
...
def get_cell_list_contents(self, cell_list: List[NetworkCoordinate]) -> List[Agent]:
"""Get agents on specified nodes."""
...
def iter_neighbors(self, node_id: NetworkCoordinate) -> Iterator[Agent]:
"""Iterate over agents on neighboring nodes."""
...The modern discrete spatial system introduces a cell-centric approach with enhanced capabilities for property-rich spatial modeling. This system is under active development and represents the future of Mesa's spatial functionality.
Active positions that can have properties and contain agents.
from mesa.discrete_space import Cell
class Cell:
"""
Active positions that can have properties and contain agents.
Cells are first-class objects that can store properties, execute behaviors,
and manage their agent occupants.
"""
def __init__(self, coordinate: Coordinate, capacity: int | None = None):
"""
Initialize a cell.
Parameters:
coordinate: The cell's position coordinate
capacity: Maximum number of agents (None for unlimited)
"""
...
def add_agent(self, agent):
"""Add an agent to this cell."""
...
def remove_agent(self, agent):
"""Remove an agent from this cell."""
...
@property
def agents(self) -> List[Agent]:
"""Get all agents in this cell."""
...
@property
def coordinate(self) -> Coordinate:
"""Get the cell's coordinate."""
...
@property
def is_empty(self) -> bool:
"""Check if cell contains no agents."""
...
@property
def is_full(self) -> bool:
"""Check if cell is at capacity."""
...
def get_neighborhood(self, radius: int = 1) -> 'CellCollection':
"""Get neighboring cells within radius."""
...Base agent class that understands cell-based spatial interactions.
from mesa.discrete_space import CellAgent
class CellAgent(Agent):
"""
Base agent class that understands cell-based spatial interactions.
Extends the basic Agent class with cell-aware spatial behavior
and movement capabilities.
"""
def __init__(self, model, cell: Cell | None = None):
"""
Initialize a CellAgent.
Parameters:
model: The model instance
cell: Initial cell position (optional)
"""
super().__init__(model)
...
@property
def cell(self) -> Cell | None:
"""Get the cell this agent occupies."""
...
@property
def coordinate(self) -> Coordinate | None:
"""Get the agent's coordinate position."""
...
def move_to(self, cell: Cell):
"""
Move this agent to a different cell.
Parameters:
cell: Target cell to move to
"""
...
def get_neighbors(self, radius: int = 1, include_center: bool = False) -> List['CellAgent']:
"""
Get neighboring agents within radius.
Parameters:
radius: Search radius in cells
include_center: Whether to include agents in same cell
"""
...
def get_neighborhood_cells(self, radius: int = 1) -> 'CellCollection':
"""Get neighboring cells within radius."""
...Specialized agent class for immobile entities permanently fixed to cells.
from mesa.discrete_space import FixedAgent
class FixedAgent(Agent):
"""
Agent permanently fixed to a cell position.
FixedAgent represents patches or immobile entities that are bound to
specific cells and cannot move. Useful for terrain features, buildings,
or other static environmental elements.
"""
def remove(self):
"""Remove the agent from the model and cell."""
...Specialized agent class with enhanced 2D grid movement capabilities.
from mesa.discrete_space import Grid2DMovingAgent
class Grid2DMovingAgent(CellAgent):
"""
Mixin for agents with directional movement in 2D grids.
Provides convenient movement methods using cardinal and ordinal directions,
with support for string-based direction commands like 'north', 'southeast', etc.
"""
DIRECTION_MAP = {
"n": (-1, 0), "north": (-1, 0), "up": (-1, 0),
"s": (1, 0), "south": (1, 0), "down": (1, 0),
"e": (0, 1), "east": (0, 1), "right": (0, 1),
"w": (0, -1), "west": (0, -1), "left": (0, -1),
"ne": (-1, 1), "northeast": (-1, 1), "upright": (-1, 1),
"nw": (-1, -1), "northwest": (-1, -1), "upleft": (-1, -1),
"se": (1, 1), "southeast": (1, 1), "downright": (1, 1),
"sw": (1, -1), "southwest": (1, -1), "downleft": (1, -1)
}
def move_by_direction(self, direction: str | tuple[int, int]):
"""
Move agent by direction string or coordinate offset.
Parameters:
direction: Direction string (e.g., 'north') or coordinate tuple
"""
...Base grid implementation for cell spaces.
from mesa.discrete_space import Grid
class Grid(DiscreteSpace):
"""
Base grid implementation for cell spaces.
Provides orthogonal grid structure with configurable neighborhood types
and boundary conditions.
"""
def __init__(self, width: int, height: int, torus: bool = False,
capacity: int | None = None):
"""
Initialize a grid.
Parameters:
width: Grid width in cells
height: Grid height in cells
torus: Whether grid wraps at boundaries
capacity: Default cell capacity (None for unlimited)
"""
...
def place_agent(self, agent: CellAgent, cell: Cell):
"""Place an agent in a specific cell."""
...
def move_agent(self, agent: CellAgent, cell: Cell):
"""Move an agent to a different cell."""
...
def remove_agent(self, agent: CellAgent):
"""Remove an agent from the grid."""
...
def select_random_empty_cell(self) -> Cell:
"""Select a random empty cell."""
...
def get_cell(self, coordinate: Coordinate) -> Cell:
"""Get cell at specific coordinate."""
...
def get_cells(self) -> CellCollection:
"""Get all cells in the grid."""
...
def get_empty_cells(self) -> CellCollection:
"""Get all empty cells."""
...
@property
def width(self) -> int:
"""Grid width."""
...
@property
def height(self) -> int:
"""Grid height."""
...Grid with 8-neighbor (Moore) connectivity pattern.
from mesa.discrete_space import OrthogonalMooreGrid
class OrthogonalMooreGrid(Grid):
"""
Grid with 8-neighbor (Moore) connectivity.
Provides Moore neighborhood where each cell has up to 8 neighbors
(including diagonal connections). In n-dimensional space, provides
(3^n)-1 neighbors per cell.
"""
def __init__(self, width: int, height: int, torus: bool = False, capacity: int | None = None):
"""
Initialize Moore grid.
Parameters:
width: Grid width
height: Grid height
torus: Whether grid wraps around edges
capacity: Maximum agents per cell (None for unlimited)
"""
...Grid with 4-neighbor (Von Neumann) connectivity pattern.
from mesa.discrete_space import OrthogonalVonNeumannGrid
class OrthogonalVonNeumannGrid(Grid):
"""
Grid with 4-neighbor (Von Neumann) connectivity.
Provides Von Neumann neighborhood where each cell has up to 4 neighbors
(only orthogonal connections, no diagonals). In n-dimensional space,
provides 2n neighbors per cell.
"""
def __init__(self, width: int, height: int, torus: bool = False, capacity: int | None = None):
"""
Initialize Von Neumann grid.
Parameters:
width: Grid width
height: Grid height
torus: Whether grid wraps around edges
capacity: Maximum agents per cell (None for unlimited)
"""
...Hexagonal grid arrangement with 6-neighbor connectivity.
from mesa.discrete_space import HexGrid
class HexGrid(DiscreteSpace):
"""
Hexagonal grid arrangement with 6-neighbor connectivity.
Provides hexagonal tessellation for models requiring 6-neighbor
spatial relationships.
"""
def __init__(self, width: int, height: int, torus: bool = False):
"""
Initialize hexagonal grid.
Parameters:
width: Grid width in hexagonal cells
height: Grid height in hexagonal cells
torus: Whether grid wraps at boundaries
"""
...
# Inherits base grid methods with hexagonal geometryNetwork-based cell arrangement using graph structures.
from mesa.discrete_space import Network
import networkx as nx
class Network(DiscreteSpace):
"""
Network-based cell arrangement using graph structures.
Enables spatial modeling on arbitrary network topologies
using NetworkX graphs.
"""
def __init__(self, graph: nx.Graph):
"""
Initialize network space.
Parameters:
graph: NetworkX graph defining connectivity
"""
...
def get_cell(self, node_id) -> Cell:
"""Get cell corresponding to network node."""
...
def get_neighbors(self, cell: Cell) -> CellCollection:
"""Get neighboring cells in the network."""
...Property layers provide efficient storage and manipulation of spatial properties across cells.
from mesa.discrete_space import PropertyLayer
class PropertyLayer:
"""
Efficient property storage and manipulation for spatial models.
Enables storing and updating scalar or array properties across
spatial cells with optimized operations.
"""
def __init__(self, name: str, width: int, height: int,
default_value: Any = 0, dtype=None):
"""
Initialize property layer.
Parameters:
name: Layer name identifier
width: Layer width
height: Layer height
default_value: Default cell value
dtype: NumPy data type
"""
...
def set_cell(self, coordinate: Coordinate, value: Any):
"""Set value at specific coordinate."""
...
def get_cell(self, coordinate: Coordinate) -> Any:
"""Get value at specific coordinate."""
...
def set_cells(self, coordinates: List[Coordinate], values: Any):
"""Set values at multiple coordinates."""
...
def get_cells(self, coordinates: List[Coordinate]) -> List[Any]:
"""Get values at multiple coordinates."""
...
def modify_cells(self, coordinates: List[Coordinate],
operation: Callable, *args, **kwargs):
"""Apply operation to modify cell values."""
...
def apply_operation(self, operation: Callable, *args, **kwargs):
"""Apply operation to all cells."""
...
@property
def data(self) -> np.ndarray:
"""Access underlying NumPy array."""
...from mesa import Agent, Model
from mesa.space import MultiGrid
from mesa.datacollection import DataCollector
class TraditionalAgent(Agent):
def __init__(self, model, energy=100):
super().__init__(model)
self.energy = energy
def step(self):
# Move to random neighboring cell
neighbors = self.model.grid.get_neighborhood(
self.pos, moore=True, include_center=False
)
new_pos = self.random.choice(neighbors)
self.model.grid.move_agent(self, new_pos)
# Interact with agents in same cell
cellmates = self.model.grid.get_cell_list_contents([self.pos])
for other in cellmates:
if other != self and isinstance(other, TraditionalAgent):
# Simple energy transfer
transfer = min(5, self.energy // 2)
self.energy -= transfer
other.energy += transfer
class TraditionalModel(Model):
def __init__(self, n_agents=50, width=20, height=20):
super().__init__()
# Create traditional grid
self.grid = MultiGrid(width, height, torus=True)
# Create agents and place randomly
for i in range(n_agents):
agent = TraditionalAgent(self, energy=100)
x = self.random.randrange(width)
y = self.random.randrange(height)
self.grid.place_agent(agent, (x, y))
self.running = True
def step(self):
self.agents.shuffle_do("step")
# Usage
model = TraditionalModel(n_agents=30, width=15, height=15)
for i in range(50):
model.step()from mesa import Model
from mesa.discrete_space import Grid, CellAgent, PropertyLayer
class ModernAgent(CellAgent):
def __init__(self, model, cell=None, energy=100):
super().__init__(model, cell)
self.energy = energy
def step(self):
# Get neighboring cells
neighbors = self.get_neighborhood_cells(radius=1)
empty_neighbors = neighbors.select_cells(lambda c: c.is_empty)
if empty_neighbors:
# Move to random empty neighboring cell
new_cell = self.random.choice(empty_neighbors)
self.move_to(new_cell)
# Interact with nearby agents
nearby_agents = self.get_neighbors(radius=1, include_center=True)
for other in nearby_agents:
if other != self:
# Energy sharing
avg_energy = (self.energy + other.energy) / 2
self.energy = avg_energy
other.energy = avg_energy
# Update cell property
if hasattr(self.model, 'temperature_layer'):
current_temp = self.model.temperature_layer.get_cell(self.coordinate)
# Agents increase local temperature
self.model.temperature_layer.set_cell(self.coordinate, current_temp + 0.1)
class ModernModel(Model):
def __init__(self, n_agents=50, width=20, height=20):
super().__init__()
# Create modern grid
self.space = Grid(width, height, torus=True)
# Create property layer for temperature
self.temperature_layer = PropertyLayer(
"temperature", width, height, default_value=20.0
)
# Create agents and place randomly
for i in range(n_agents):
agent = ModernAgent(self, energy=100)
empty_cell = self.space.select_random_empty_cell()
self.space.place_agent(agent, empty_cell)
self.running = True
def step(self):
# Execute agent behaviors
self.agents.shuffle_do("step")
# Update environment - cool down temperature
self.temperature_layer.apply_operation(
lambda temp: max(20.0, temp * 0.95) # Cool towards base temperature
)
# Usage
model = ModernModel(n_agents=30, width=15, height=15)
for i in range(50):
model.step()
# Access spatial data
hot_cells = []
for cell in model.space.get_cells():
temp = model.temperature_layer.get_cell(cell.coordinate)
if temp > 25.0:
hot_cells.append(cell)
print(f"Hot spots: {len(hot_cells)} cells above 25°C")from mesa import Agent, Model
from mesa.space import ContinuousSpace
import math
class ContinuousAgent(Agent):
def __init__(self, model, pos, speed=1.0):
super().__init__(model)
self.pos = pos
self.speed = speed
self.heading = self.random.uniform(0, 2 * math.pi)
def step(self):
# Move in current direction
dx = math.cos(self.heading) * self.speed
dy = math.sin(self.heading) * self.speed
self.model.space.move_by(self, dx, dy)
# Avoid crowding - turn away from nearby agents
neighbors = self.model.space.get_neighbors(self.pos, radius=3.0)
if len(neighbors) > 3:
# Calculate average position of neighbors
avg_x = sum(n.pos[0] for n in neighbors) / len(neighbors)
avg_y = sum(n.pos[1] for n in neighbors) / len(neighbors)
# Turn away from crowd
away_heading = self.model.space.get_heading(
(avg_x, avg_y), self.pos
)
self.heading = away_heading + self.random.uniform(-0.5, 0.5)
# Random walk component
self.heading += self.random.uniform(-0.1, 0.1)
class ContinuousModel(Model):
def __init__(self, n_agents=100, width=50.0, height=50.0):
super().__init__()
# Create continuous space
self.space = ContinuousSpace(width, height, torus=True)
# Create agents at random positions
for i in range(n_agents):
x = self.random.uniform(0, width)
y = self.random.uniform(0, height)
agent = ContinuousAgent(self, (x, y), speed=self.random.uniform(0.5, 2.0))
self.space.place_agent(agent, (x, y))
self.running = True
def step(self):
self.agents.shuffle_do("step")
# Usage
model = ContinuousModel(n_agents=50, width=30.0, height=30.0)
for i in range(100):
model.step()from mesa import Agent, Model
from mesa.space import NetworkGrid
import networkx as nx
class NetworkAgent(Agent):
def __init__(self, model, node_id):
super().__init__(model)
self.node_id = node_id
self.infection_status = "susceptible"
def step(self):
if self.infection_status == "infected":
# Spread infection to neighbors
neighbors = self.model.grid.get_neighbors(self.pos)
for neighbor_node in neighbors:
neighbor_agents = self.model.grid.get_cell_list_contents([neighbor_node])
for agent in neighbor_agents:
if (agent.infection_status == "susceptible" and
self.random.random() < 0.1): # 10% transmission rate
agent.infection_status = "infected"
elif self.infection_status == "susceptible":
# Random movement
neighbors = self.model.grid.get_neighbors(self.pos)
if neighbors:
new_node = self.random.choice(neighbors)
self.model.grid.move_agent(self, new_node)
class NetworkModel(Model):
def __init__(self, network_type="small_world", n_nodes=100):
super().__init__()
# Create network
if network_type == "small_world":
G = nx.watts_strogatz_graph(n_nodes, 6, 0.3)
elif network_type == "scale_free":
G = nx.barabasi_albert_graph(n_nodes, 3)
else:
G = nx.erdos_renyi_graph(n_nodes, 0.1)
self.grid = NetworkGrid(G)
# Create agents on random nodes
nodes = list(G.nodes())
for i, node in enumerate(nodes):
agent = NetworkAgent(self, node)
self.grid.place_agent(agent, node)
# Patient zero
if self.agents:
patient_zero = self.random.choice(self.agents)
patient_zero.infection_status = "infected"
self.running = True
def step(self):
self.agents.shuffle_do("step")
# Check if infection has spread to everyone or died out
infected = len([a for a in self.agents if a.infection_status == "infected"])
if infected == 0 or infected == len(self.agents):
self.running = False
# Usage
model = NetworkModel("small_world", n_nodes=50)
for i in range(100):
model.step()
if not model.running:
break
infected_count = len([a for a in model.agents if a.infection_status == "infected"])
print(f"Final infected count: {infected_count}/{len(model.agents)}")# Traditional approach
from mesa.space import MultiGrid
class OldModel(Model):
def __init__(self):
self.grid = MultiGrid(10, 10, True)
# Traditional grid operations...
# Modern approach - similar functionality with enhanced capabilities
from mesa.discrete_space import Grid
class NewModel(Model):
def __init__(self):
self.space = Grid(10, 10, torus=True)
# Modern grid operations with cells and property layers...