Modelly: agent-based modeling in Python#

Agent-based models in Python, fast enough to explore.

Modelly is an open-source framework for agent-based modeling. Write agents with a step(), keep their state in numpy columns as the model grows, and get the same result from the same seed every time.

Click or drag to infect cells
Susceptible0Infected0Recovered0 Step 0

The same models, 4.3 to 213 times faster than Mesa

Eight playbooks, each written twice in the idiom of its framework. Median CPU seconds of the steps over three rounds, a fresh interpreter per run, pinned to one core.

Grid epidemic20,000 agents, 60 steps
Mesa 2.3 11.54 s
Modelly, cell lists 5.92 s
Modelly, columns 0.21 s
56x
Predator-prey~5,500 agents, 200 steps
Mesa 2.3 3.90 s
Modelly, cell lists 2.82 s
Modelly, columns 0.47 s
8.3x
Flocking4,000 agents, 50 steps
Mesa 2.3 27.91 s
Modelly, columns 0.72 s
39x
Virus on a network30,000 agents, 100 steps
Mesa 2.3 8.70 s
Modelly, columns 0.20 s
44x
Insects, individuals~26,000 agents, 120 days
Mesa 2.3 2.83 s
Modelly, columns 0.65 s
4.3x
Insects, cohorts~26,000 agents, 120 days
Mesa 2.3 2.83 s
Modelly, cohorts 0.01 s
213x
Schelling segregation~32,000 residents, 40 steps
Mesa 2.3 3.23 s
Modelly, cell lists 0.22 s
Modelly, columns 0.12 s
27x
Boltzmann wealth10,000 agents, 100 steps
Mesa 2.3 1.51 s
Modelly, columns 0.20 s
7.5x
Sugarscape1,600 foragers, 100 steps
Mesa 2.3 5.57 s
Modelly, cell lists 0.88 s
Modelly, columns 0.18 s
31.5x

Same answersEvery variant runs the same seeds. No summary statistic differs from Mesa's under Welch's t-test at p < 0.01.

Same seed, same runThe first seed runs twice per implementation and must reproduce exactly.

Same code, tooA research model ported line for line from Mesa 2.3, with nothing vectorised, runs 12% faster on Modelly with bit-identical output, from the compiled random core alone.

Re-run it yourselfThe playbooks and the runner are in benchmarks/playbooks; the method is on the performance page.

What people model with it

Each sketch below is a small version of a playbook or a shipped example, running in your browser.

Epidemics on a grid

Susceptible, infected and recovered agents on a 20,000-cell torus. Exposure is one count over the grid's adjacency, so 60 steps take 0.22 s.

Read the playbook

Predators, prey and grass

Wolves and sheep are born and die every step while the grass grows back. Creating and removing agents costs what it costs in Mesa; walking and hunting are vectorised.

Open the Wolf-Sheep example

Flocks in continuous space

Boids align, gather and keep their distance through neighbour search in continuous space: 4,000 birds, 50 steps, about a second.

Open the Boids example

Contagion on a network

A virus moves along the edges of a 30,000-node random network. Recovered nodes can gain resistance or become susceptible again.

Open the network example

Pest control by sterile release

A stage-structured insect population under sterile-male releases, the shaded bands. Grouping identical insects into cohorts keeps the law of the individual model and runs 120 days in 13 ms.

See how cohorts stay exact

Segregation from mild preferences

Schelling's model: agents who only want half their neighbours to match them still end up in sharply separated neighbourhoods.

Open the Schelling example

A model in three pieces

Boltzmann's wealth model: agents wander a grid and hand a coin to whoever shares their cell.

  1. A scenario holds the inputs: population, grid size, seed.
  2. Agents each know one thing: how to take their own step.
  3. A model owns the world and moves it forward in time.
from modelly import Model
from modelly.space import CellAgent, OrthogonalMooreGrid
from modelly.scenarios import Scenario


class WealthScenario(Scenario):
    n: int = 120
    width: int = 12
    height: int = 12


class MoneyAgent(CellAgent):
    def __init__(self, model, cell):
        super().__init__(model)
        self.cell = cell
        self.wealth = 1

    def step(self):
        self.cell = self.cell.neighborhood.select_random_cell()
        cellmates = [a for a in self.cell.agents if a is not self]
        if self.wealth > 0 and cellmates:
            self.random.choice(cellmates).wealth += 1
            self.wealth -= 1


class WealthModel(Model):
    def __init__(self, scenario: WealthScenario = WealthScenario):
        super().__init__(scenario=scenario)
        self.grid = OrthogonalMooreGrid(
            (scenario.width, scenario.height), random=self.random
        )
        cells = self.random.choices(self.grid.all_cells.cells, k=scenario.n)
        MoneyAgent.create_agents(self, scenario.n, cells)

    def step(self):
        self.agents.shuffle_do("step")


model = WealthModel()
for _ in range(100):
    model.step()

Everything a simulation study needs

Scenario
Parameters as a typed class: one run, one seed, one config object.
Agents
Classes with a step(), grouped in ordered AgentSets.
Columns
Attributes stored in numpy arrays, readable per agent and as a whole.
Space
Square and hexagonal grids, networks, Voronoi meshes, continuous space.
Time
Ticks, scheduled events, generators and priorities in one scheduler.
Data
Reporters and datasets that land in pandas, ready to plot.
Visualization
Browser dashboards with Solara, matplotlib and altair charts, notebooks.
Replications
Seeded parameter sweeps spread over processes.