Skip to main content

Agent-based Modeling with Blazingly Efficient Records

Project description

AMBER (Agent-based Modeling with Blazingly Efficient Records)

CI codecov Python 3.9+ PyPI version License: BSD-3-Clause

AMBER is a Python framework for agent-based modeling that uses Polars for efficient data handling and analysis. AMBER provides a clean, robust API for creating parallel, high-performance simulations in Python.

🚀 Performance

AMBER stores the entire population as a columnar Polars DataFrame and exposes a vectorized view API (agents.where(...), agents.at[ids], scatter_add) that compiles per-step updates down to a handful of Polars expressions — regardless of population size.

Benchmark against six other representative ABM/simulation frameworks — 5000 agents, 50 executed steps, Python 3.12, Julia 1.12.3, Apple Silicon. All numbers are seeded wall-clock timings, averaged over 10 runs (slowest trimmed). Every framework is checked against output invariants (wealth conservation, boundary clamping, S+I+R population conservation) before timing — see benchmarks/correctness_check.py. Reproducer: benchmarks/run_all_frameworks.py.

Framework Language Arch. Wealth Transfer Random Walk SIR Epidemic
AMBER (vectorized) Python Columnar (Polars) 20 ms 4.8 ms 497 ms
Agents.jl Julia Object 7.2 ms 1.6 ms 813 ms
AMBER (loop) Python Object 169 ms 332 ms 9.53 s
Mesa Python Object 22.61 s 131 ms 16.63 s
AgentPy Python Object 266 ms 141 ms 10.98 s
SimPy Python Event loop 216 ms 254 ms 4.67 s
Melodie Python Hybrid 177 ms 1.03 s 20.09 s

AMBER (vectorized) is the fastest Python-hosted framework on every headline model at 5000 agents. The headline SIR row is schedule-mixed; use it as workload-class timing, not as an equivalent-trajectory AMBER-over-Julia claim. Against Agents.jl, AMBER trails the Julia implementation on wealth transfer and random walk, where per-step work is small enough that Julia's compiled dispatch has less fixed overhead.

Seven-framework scaling chart

See benchmarks/README.md for the full table at 500 / 1000 / 5000 agents, speedup ratios, a per-model correctness audit, and the documented SIR update-semantics caveat.

Scaling to 10M agents with the GPU backend

The 0.4 GPU backend changes the story at large N. This sweep covers 1k → 10M agents across 10 frameworks and four models — adding AMBER (GPU) and a Schelling segregation workload (NVIDIA RTX 3090 for the GPU series):

AMBER GPU + Schelling scaling to 10M agents across 10 frameworks

  • AMBER (GPU) reaches 10M agents on all four models while CPU frameworks blow up. Wealth transfer: ~14 ms at 1M (≈330× the fastest CPU framework), 199 ms at 10M (3.1× faster than FLAME GPU 2). Schelling: the only framework to reach 10M (847 ms; 19× AMBER-vectorized and 225× Agents.jl at 1M). SIR: 5.98 s at 10M (~2× faster than FLAME GPU 2), via an O(N) spatial-binning kernel.
  • It's a large-N win, not a small-N one. A ~90 ms fixed device cost means AMBER (GPU) only leads at scale: below ~100k–1M, AMBER (vectorized) or Agents.jl are faster, and on SIR FLAME GPU 2 wins at 1k–10k before AMBER overtakes it from 100k up. (FLAME GPU 2 implements no Schelling model, so it is absent from that panel.)

Regenerate with python benchmarks/plot_scaling_with_gpu_schelling.py.

🚀 Quick Start

import ambr as am
import numpy as np

# Define a model with the vectorized view API — no per-agent loops.
class WealthModel(am.Model):
    # Declarative metric: evaluated once per step into results['model'].
    model_reporters = {'total_wealth': lambda m: int(m.agents.wealth.sum())}

    def setup(self):
        self.add_agents(100, wealth=self.rng.integers(1, 10, size=100))

    def step(self):
        donors = self.agents.where(self.agents.wealth > 0)
        donors.wealth -= 1
        recipients = self.rng.choice(self.agents.ids.to_numpy(), size=len(donors))
        self.agents.at[recipients].scatter_add(wealth=1)

model = WealthModel({'steps': 100, 'seed': 42, 'show_progress': False})
results = model.run()
print(results['model'].tail(5))   # per-step total_wealth
print(results['agents'].head(10)) # final agent state

self.rng is the canonical seeded RNG (a NumPy Generator); self.random is the stdlib one. Both are seeded from the seed parameter.

New in 0.4: a runtime snapshot-view contract checker, a GPU backend + batched calibration, one canonical verb per task (legacy spellings still work), declarative model_reporters, and a typed params schema. See the changelog.

New in 0.3.0: Setting agent.wealth = 5 on a Python Agent automatically syncs to the DataFrame. You can freely mix OOP-style and vectorized access without desync.

⚡ Vectorized View API

The view API compiles per-step updates to a handful of Polars expressions — regardless of population size:

def step(self):
    # Bulk columnar reads/writes over the entire population
    self.agents.x = self.agents.x + self.rng.uniform(-1, 1, len(self.agents))

    # Filtered writes: only agents matching a condition
    infected = self.agents.where(self.agents.status == 1)
    infected.infection_time += 1

    # scatter_add: flow-of-resources with duplicate-id safety
    self.agents.at[[1, 1, 3]].scatter_add(wealth=1)  # agent 1 gets +2, agent 3 gets +1

🧭 Canonical API (0.4)

AMBER 0.4 settles on one obvious verb per task. The legacy spellings still work (they emit a DeprecationWarning and are scheduled for removal in 1.0); set AMBER_SUPPRESS_DEPRECATIONS=1 to silence them in benchmark / reproducibility runs.

Task Canonical Legacy (deprecated)
NumPy RNG self.rng self.nprandom
Record a model metric model_reporters = {...} or record_model(k, v) record(k, v)
Filter agents agents.where(expr) / agents[mask] / agents.at[ids] agents.select(...)
Per-agent write agent.col = v agent.record(...), agent.update_data(...)
Bulk write agents.set(**cols) agents.record(...), agents.update_data(...)
Read agent objects iterate model.agents, agents.by_id(i) agents.agents, agents.agent_ids
Bulk numpy round-trip agents.numpy(...) + agents.set(...) (or borrow/commit) .to_numpy() + per-column assign
Typed parameters params = {'n': (int, 200)}, then self.p.n int(self.p.get('n', 200))
Grid wrap GridEnvironment(torus=True) wrap= / .wrap

update() is now a pure hook — overriding it no longer requires super().update(). Declare model_reporters / agent_reporters for declarative metrics, and set record_initial = True to capture a t=0 row.

🔒 Snapshot-view contract

The columnar fast path is only valid for rules that preserve the intended update schedule. AMBER turns that from a silent assumption into a checkable, per-step artifact — run with a contract mode and inspect the certificates:

results = model.run(steps=100, contract="check")   # "off" | "check" | "warn" | "raise"
for cert in results["contract"]:
    if not cert.ok:
        print(cert.step, cert.violations)

check records a ContractCertificate per step; warn also emits a warning per violation; raise stops on the first one (e.g. a same-step read-after-write, or a duplicate ordinary assignment that no snapshot rewrite can reproduce). Mode off (the default) adds zero overhead.

🎮 GPU backend & batched calibration

The same columnar contract runs on a CuPy array backend, and the ensemble axis (B simulations × N agents) batches into one device pass — the natural fit for calibration, where you evaluate thousands of small replicate runs:

from ambr.gpu_ensemble import GPUEnsembleRunner, BatchedWellMixedSIR, smac_batch_calibrate

# Evaluate B parameter sets in one (B, N) GPU pass
runner = GPUEnsembleRunner(BatchedWellMixedSIR())
traj = runner.run(n_agents=100_000, steps=60,
                  params={"beta": betas, "gamma": gammas, "i0_frac": i0})  # -> {metric: (B, steps)}

# SMAC ask -> one batched GPU evaluation -> tell
best, history = smac_batch_calibrate(BatchedWellMixedSIR(), bounds, loss_fn,
                                     n_agents=100_000, steps=60)

ambr.gpu provides the array-module abstraction (get_array_module, to_device, to_host) and falls back to NumPy when CuPy is unavailable.

🔬 Optimization

AMBER includes powerful optimization capabilities for parameter tuning:

from ambr.optimization import ParameterSpace, grid_search

# Define parameter space
parameter_space = ParameterSpace({
    'agents': [10, 50, 100],
    'initial_value': [1, 5, 10],
    'steps': 100
})

# Run optimization
results = grid_search(MyModel, parameter_space, 'some_metric')
best_params = results[0]['parameters']

Beyond grid_search, AMBER ships random_search, bayesian_optimization (SMAC Gaussian-process), and SMACOptimizer (random-forest surrogate) — plus the GPU batched ensemble above for derivative-free calibration at scale.

📦 Installation

pip install ambr

🏗️ Features

  • Simple API: Intuitive interface for agent-based modeling
  • High Performance: Efficient data handling with Polars DataFrames
  • Snapshot-view contract: runtime conformance checking that columnar updates preserve the intended schedule
  • GPU backend: CuPy array backend + batched ensemble for parameter sweeps and calibration
  • Optimization: grid / random / Bayesian (SMAC) search, plus GPU-batched calibration
  • Declarative reporting: model_reporters / agent_reporters and a typed params schema
  • Environments: Support for grid, network, and continuous space environments
  • Experiments: Run multiple simulations with parameter sampling
  • Random Number Generation: Reproducible simulations with controlled randomness

📚 Examples

Working examples are available in the examples/ directory:

  • Wealth Transfer Model: Economic inequality simulation
  • Virus Spread Model: Epidemiological SIR model
  • Flocking Simulation: Boids flocking behavior
  • Forest Fire Model: Cellular automata fire spread
  • Network Simulations: Graph-based agent interactions

📖 Documentation

📝 How to cite?

If you use AMBER in your research, please cite our paper:

@article{pham2026amber,
  title={AMBER: A Columnar Architecture for High-Performance Agent-Based Modeling in Python},
  author={Pham, Anh-Duy},
  journal={arXiv preprint arXiv:2601.16292},
  year={2026}
}

🤝 Contributing

We welcome contributions! Please see our contributing guidelines for more information.

📄 License

This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ambr-0.4.0.tar.gz (61.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ambr-0.4.0-py3-none-any.whl (60.8 kB view details)

Uploaded Python 3

File details

Details for the file ambr-0.4.0.tar.gz.

File metadata

  • Download URL: ambr-0.4.0.tar.gz
  • Upload date:
  • Size: 61.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.4

File hashes

Hashes for ambr-0.4.0.tar.gz
Algorithm Hash digest
SHA256 05879e41e77710cbf05ad875341b5548adcd7edd4a628a3c9a40efe056e8ebe5
MD5 87675cda202d26b428d966afe4b72827
BLAKE2b-256 30c25f1ed85a4559e010abe6f6531aae4120368e3a5d5103abb82bcad624dc49

See more details on using hashes here.

File details

Details for the file ambr-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: ambr-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 60.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.4

File hashes

Hashes for ambr-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2e212282cda3dc5e70385f0f20158b5910e1668dde86b71b63e886ff7277267b
MD5 d103a0bc6652b8f6677dc1aea95b2803
BLAKE2b-256 d1ca315a7b7980668792ecfdc46cc115dc0d605dca7b20a08259ffbe31a7e9f5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page