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., |
System |
Event |
A function that reads and modifies role data each simulation period (e.g., |
Relationship |
A sparse many-to-many connection between agents with edge data (e.g., |
|
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 |
Produce goods, hire workers, borrow from banks |
|
Households |
Work for firms, consume goods, receive dividends |
|
Banks |
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:
Planning: Firms set production targets and labor needs
Labor Market: Workers search for jobs, firms hire
Credit Market: Firms borrow from banks to finance production
Production: Firms pay wages and produce goods
Goods Market: Households shop for consumption goods
Revenue: Firms collect revenue, repay debts, pay dividends
Bankruptcy: Insolvent firms and banks exit
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:
@roleregisters a new role class@eventregisters a new event class@relationshipregisters 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#
Understand the economic theory and mathematical rules behind each phase |
|
Learn how to initialize and run simulations |
|
Jump straight into a working example |
|
Configure parameters and customize behavior |
|
Explore built-in model extensions (R&D, buffer-stock, taxation) |