Overview#

What is BAM Engine?#

BAM Engine is a high-performance Python framework for agent-based macroeconomic simulation. It implements the BAM (Bottom-Up Adaptive Macroeconomics) model, part of the CATS (Complex Adaptive Trivial Systems) family of models originally described in Macroeconomics from the Bottom-up by Delli Gatti, Gaffeo, Gallegati, Giulioni, and Palestrini (2011).

The model simulates an economy populated by three types of agents (firms, households, and banks) interacting through decentralized markets for labor, credit, and consumption goods. Unlike traditional DSGE models, BAM uses bounded rationality and adaptive behavior: agents follow simple heuristic rules rather than solving optimization problems, and macroeconomic patterns (business cycles, unemployment dynamics, firm size distributions) emerge from the bottom up through agent interactions.

BAM Engine brings this model to Python with a design focused on performance, modularity, and extensibility. All agent state is stored as NumPy arrays and operations are fully vectorized, making simulations fast enough for large-scale parameter sweeps and calibration studies.

Architecture#

BAM Engine uses an Entity-Component-System (ECS) inspired architecture, adapted for agent-based economic modeling:

ECS Concept

BAM Engine

Description

Entity

Agent

A lightweight identifier (integer ID) for a firm, household, or bank

Component

Role

A data container holding agent state as parallel NumPy arrays (e.g., Producer, Worker, Lender)

System

Event

A function that reads and modifies role data each simulation period (e.g., FirmsAdjustPrice)

Relationship

A sparse many-to-many connection between agents with edge data (e.g., LoanBook)

Pipeline

An ordered sequence of events executed each period

Why ECS? Traditional object-oriented agent models store state on individual agent objects, leading to scattered memory access and Python-loop iteration. The ECS pattern stores all agents of the same type in contiguous NumPy arrays, enabling vectorized operations across entire populations in a single call. This gives BAM Engine near-C performance while keeping the code readable and modular.

The architecture separates data (roles) from behavior (events), so you can add new agent attributes or behavioral rules without touching existing code.

Key Concepts#

Agents#

The BAM economy contains three agent types, each composed of multiple roles:

Agent Type

Roles

Description

Firms

Producer, Employer, Borrower

Produce goods, hire workers, borrow from banks

Households

Worker, Consumer, Shareholder

Work for firms, consume goods, receive dividends

Banks

Lender

Supply credit to firms, earn interest income

Each agent is identified by an integer index (0 to N-1) shared across all its roles. For example, firm 7’s production data is at Producer.production[7], its wage offer at Employer.wage_offer[7], and its net worth at Borrower.net_worth[7].

Roles#

Roles are dataclass-like containers where each field is a NumPy array indexed by agent ID. They hold all agent state:

prod = sim.get_role("Producer")
prod.price  # shape (n_firms,) -- current price per firm
prod.inventory  # shape (n_firms,) -- unsold goods per firm

Roles are defined with the @role decorator and type annotations:

from bamengine import role
from bamengine.typing import Float, Int


@role
class Inventory:
    goods_on_hand: Float
    reorder_point: Float
    days_until_delivery: Int

See Custom Roles for the full guide.

Events#

Events are the behavioral rules of the simulation. Each event reads role data, performs calculations, and writes results back, typically using the ops module for safe, vectorized operations:

from bamengine import event, ops


@event(after="firms_collect_revenue")
class FirmsCalcBonus:
    def execute(self, sim):
        bor = sim.get_role("Borrower")
        bonus = ops.where(bor.net_profit > 0, ops.multiply(bor.net_profit, 0.05), 0)
        ops.assign(bor.total_funds, ops.add(bor.total_funds, bonus))

Events are executed in a fixed order defined by the pipeline. See Custom Events for the full guide.

Relationships#

Relationships represent many-to-many connections between agents, stored in a sparse COO (Coordinate) format. The built-in LoanBook tracks loans between firms and banks:

loans = sim.get_relationship("LoanBook")
loans.principal  # principal amount per active loan
loans.rate  # interest rate per active loan
loans.debt_per_borrower(n_borrowers=sim.n_firms)  # total debt per firm

See Custom Relationships for the full guide.

Pipeline#

The pipeline defines the execution order of events within each simulation period. BAM Engine uses explicit ordering (not dependency-based topological sort) to match the original model specification:

# Inspect the pipeline
for entry in sim.pipeline:
    print(entry.name)

The default pipeline has 8 phases: Planning, Labor Market, Credit Market, Production, Goods Market, Revenue, Bankruptcy, and Entry. See Pipeline Customization for customization options.

The Simulation Loop#

Each call to sim.step() executes one period of the economy:

  1. Planning: Firms set production targets and labor needs

  2. Labor Market: Workers search for jobs, firms hire

  3. Credit Market: Firms borrow from banks to finance production

  4. Production: Firms pay wages and produce goods

  5. Goods Market: Households shop for consumption goods

  6. Revenue: Firms collect revenue, repay debts, pay dividends

  7. Bankruptcy: Insolvent firms and banks exit

  8. Entry: Replacement firms and banks are created

See The BAM Model for the full economic detail of each phase.

Auto-Registration#

BAM Engine uses Python decorators that automatically register components in a global registry:

  • @role registers a new role class

  • @event registers a new event class

  • @relationship registers a new relationship class

Registration happens at class definition time (via __init_subclass__), so importing a module is enough to make its components available:

from bamengine import get_role, get_event, get_relationship

# Look up registered components by name
Producer = get_role("Producer")
evt_cls = get_event("firms_adjust_price")
LoanBook = get_relationship("LoanBook")

This means extensions just need to be imported before sim.run(); no explicit registration calls needed. However, pipeline hooks (after=, before=, replace=) still require explicit activation via use_events().

Deterministic RNG#

All randomness in BAM Engine flows through a single numpy.random.Generator seeded at initialization:

sim = bam.Simulation.init(seed=42)
# sim.rng is a numpy.random.Generator seeded with 42

This guarantees perfect reproducibility: the same seed always produces the same simulation trajectory, regardless of platform or Python version. Custom events should always use sim.rng rather than numpy.random directly:

# Correct: deterministic
shock = sim.rng.uniform(0, 0.1, size=sim.n_firms)

# Wrong: breaks reproducibility
shock = np.random.uniform(0, 0.1, size=sim.n_firms)

What’s Next#

The BAM Model

Understand the economic theory and mathematical rules behind each phase

Running Simulations

Learn how to initialize and run simulations

Getting Started

Jump straight into a working example

Configuration

Configure parameters and customize behavior

Model Extensions

Explore built-in model extensions (R&D, buffer-stock, taxation)