Tutorial
This tutorial will guide you through building increasingly complex agent-based models with AMBER.
Part 1: Your First Model
Let’s start with a simple wealth transfer model where agents randomly exchange money. AMBER’s view API expresses this in a handful of Polars operations — no per-agent loop.
Step 1: Define the Model
import ambr as am
class WealthModel(am.Model):
def setup(self):
# Bulk-create the population with columnar initial state.
n = self.p['n_agents']
self.add_agents(
n,
wealth=self.nprandom.integers(1, 10, size=n),
)
def step(self):
# Every agent with wealth > 0 gives $1 to a random other agent.
donors = self.agents.where(self.agents.wealth > 0)
donors.wealth -= 1
# Scatter the $1 credits. Using ``scatter_add`` (rather than a
# plain ``view.wealth = ...``) is what makes the math right when
# two donors happen to pick the same recipient.
ids = self.agents.ids.to_numpy()
recipients = self.nprandom.choice(ids, size=len(donors))
self.agents.at[recipients].scatter_add(wealth=1)
Step 2: Run the Model
# Run the simulation
model = WealthModel({
'n_agents': 100,
'steps': 100,
'seed': 42
})
results = model.run()
# Examine results
print("Final wealth distribution:")
final_wealth = results['agents'].filter(
results['agents']['step'] == results['agents']['step'].max()
)
print(final_wealth.select(['id', 'wealth']).head(10))
Part 2: Adding Spatial Structure
Now let’s enhance our model with a grid environment where agents can only interact with neighbors.
Step 1: Create Spatial Model
class SpatialWealthModel(am.Model):
def setup(self):
# Create grid environment
self.grid = am.GridEnvironment(self, size=(20, 20))
n = self.p['n_agents']
# Columnar creation: position + wealth together, no loop.
self.add_agents(
n,
wealth=self.nprandom.integers(1, 10, size=n),
x=self.nprandom.integers(0, 20, size=n),
y=self.nprandom.integers(0, 20, size=n),
)
def step(self):
# Same donor-pays-$1 idiom as Part 1 — the grid only affects
# *who* the recipients are, not the vectorized shape of the
# update. Here we keep the transfer global for simplicity; see
# ``examples/segregation_model.py`` for a neighbourhood-scoped
# variant built on ``GridEnvironment.get_neighbors``.
donors = self.agents.where(self.agents.wealth > 0)
donors.wealth -= 1
ids = self.agents.ids.to_numpy()
recipients = self.nprandom.choice(ids, size=len(donors))
self.agents.at[recipients].scatter_add(wealth=1)
Step 2: Visualize Results
import matplotlib.pyplot as plt
# Run spatial model
spatial_model = SpatialWealthModel({
'n_agents': 200,
'steps': 50,
'seed': 42
})
results = spatial_model.run()
# Plot wealth distribution on grid
final_data = results['agents'].filter(
results['agents']['step'] == results['agents']['step'].max()
)
plt.figure(figsize=(10, 8))
scatter = plt.scatter(
final_data['x'],
final_data['y'],
c=final_data['wealth'],
cmap='viridis',
s=50
)
plt.colorbar(scatter, label='Wealth')
plt.title('Final Wealth Distribution on Grid')
plt.xlabel('X Position')
plt.ylabel('Y Position')
plt.show()
Part 3: Data Collection and Analysis
Let’s add comprehensive data collection to track model-level metrics.
Step 1: Enhanced Model with Analytics
import numpy as np
class AnalyticalWealthModel(am.Model):
def setup(self):
n = self.p['n_agents']
self.add_agents(n, wealth=self.nprandom.integers(1, 10, size=n))
def step(self):
donors = self.agents.where(self.agents.wealth > 0)
donors.wealth -= 1
ids = self.agents.ids.to_numpy()
recipients = self.nprandom.choice(ids, size=len(donors))
self.agents.at[recipients].scatter_add(wealth=1)
# Polars Series aggregates are the idiomatic way to record metrics.
wealth = self.agents.wealth
self.record_model('total_wealth', int(wealth.sum()))
self.record_model('mean_wealth', float(wealth.mean()))
self.record_model('wealth_std', float(wealth.std() or 0.0))
self.record_model('gini_coefficient', self.calculate_gini(wealth.to_numpy()))
@staticmethod
def calculate_gini(values):
"""Calculate Gini coefficient of wealth inequality."""
if values.size == 0 or values.sum() == 0:
return 0.0
sorted_vals = np.sort(values)
n = len(sorted_vals)
cumsum = np.cumsum(sorted_vals)
return (n + 1 - 2 * cumsum.sum() / cumsum[-1]) / n
Step 2: Analyze Results
# Run analytical model
model = AnalyticalWealthModel({
'n_agents': 100,
'steps': 200,
'seed': 42
})
results = model.run()
# Create comprehensive analysis plots
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# Plot 1: Mean wealth over time
axes[0,0].plot(results['model']['mean_wealth'])
axes[0,0].set_title('Mean Wealth Over Time')
axes[0,0].set_xlabel('Time Step')
axes[0,0].set_ylabel('Mean Wealth')
# Plot 2: Wealth inequality (Gini coefficient)
axes[0,1].plot(results['model']['gini_coefficient'])
axes[0,1].set_title('Wealth Inequality (Gini Coefficient)')
axes[0,1].set_xlabel('Time Step')
axes[0,1].set_ylabel('Gini Coefficient')
# Plot 3: Wealth standard deviation
axes[1,0].plot(results['model']['wealth_std'])
axes[1,0].set_title('Wealth Standard Deviation')
axes[1,0].set_xlabel('Time Step')
axes[1,0].set_ylabel('Standard Deviation')
# Plot 4: Final wealth distribution histogram
final_wealth = results['agents'].filter(
results['agents']['step'] == results['agents']['step'].max()
)['wealth']
axes[1,1].hist(final_wealth, bins=20, alpha=0.7)
axes[1,1].set_title('Final Wealth Distribution')
axes[1,1].set_xlabel('Wealth')
axes[1,1].set_ylabel('Frequency')
plt.tight_layout()
plt.show()
Part 4: Parameter Optimization
Let’s use AMBER’s optimization tools to find the best parameters for our model.
Step 1: Define Optimization Target
from ambr import ParameterSpace, IntRange, grid_search
# Define parameter space to explore
param_space = ParameterSpace({
'n_agents': IntRange(50, 200),
'steps': [50, 100, 150],
'seed': IntRange(1, 10)
})
# Run grid search to minimize final Gini coefficient
best_params, best_score = grid_search(
model_class=AnalyticalWealthModel,
param_space=param_space,
metric='gini_coefficient', # Minimize inequality
minimize=True,
n_runs=3 # Average over multiple runs
)
print(f"Best parameters: {best_params}")
print(f"Best Gini coefficient: {best_score}")
Step 2: Compare Optimization Methods
from ambr import random_search, bayesian_optimization
# Compare different optimization approaches
methods = {
'grid_search': grid_search,
'random_search': random_search,
'bayesian_optimization': bayesian_optimization
}
results = {}
for name, method in methods.items():
if name == 'random_search':
best_params, best_score = method(
model_class=AnalyticalWealthModel,
param_space=param_space,
metric='gini_coefficient',
minimize=True,
n_samples=20,
n_runs=3
)
elif name == 'bayesian_optimization':
best_params, best_score = method(
model_class=AnalyticalWealthModel,
param_space=param_space,
metric='gini_coefficient',
minimize=True,
n_calls=20,
n_runs=3
)
else: # grid_search
best_params, best_score = method(
model_class=AnalyticalWealthModel,
param_space=param_space,
metric='gini_coefficient',
minimize=True,
n_runs=3
)
results[name] = {
'params': best_params,
'score': best_score
}
# Compare results
for method, result in results.items():
print(f"{method}: Gini = {result['score']:.4f}, Params = {result['params']}")
Part 5: Running Experiments
Finally, let’s use the experiment framework to run systematic parameter sweeps.
Step 1: Design Experiment
from ambr import Experiment, Sample, IntRange
# Define parameter variations
experiment_params = Sample({
'n_agents': IntRange(50, 300),
'steps': 100,
'seed': [1, 2, 3, 4, 5] # Multiple seeds for robustness
})
# Create experiment
experiment = Experiment(
model_class=AnalyticalWealthModel,
parameters=experiment_params,
iterations=50 # Number of parameter combinations to try
)
# Run experiment
experiment_results = experiment.run()
Step 2: Analyze Experiment Results
# Analyze relationship between population size and inequality
import pandas as pd
# Convert to pandas for easier analysis
df = experiment_results.to_pandas()
# Group by number of agents and calculate mean Gini coefficient
gini_by_population = df.groupby('n_agents')['gini_coefficient'].mean()
plt.figure(figsize=(10, 6))
plt.plot(gini_by_population.index, gini_by_population.values, 'o-')
plt.xlabel('Number of Agents')
plt.ylabel('Mean Gini Coefficient')
plt.title('Wealth Inequality vs Population Size')
plt.grid(True, alpha=0.3)
plt.show()
# Statistical analysis
correlation = df['n_agents'].corr(df['gini_coefficient'])
print(f"Correlation between population size and inequality: {correlation:.3f}")
Next Steps
You now have the foundation to build complex agent-based models with AMBER. Here are some directions to explore:
Custom Agent Behaviors: Create specialized agent classes with complex decision-making
Network Models: Use NetworkEnvironment for social network simulations
Multi-Agent Interactions: Implement group behaviors and collective decision-making
Real-Time Visualization: Add interactive plotting and animation
Advanced Analytics: Implement custom metrics and statistical analysis
Performance Optimization: Scale models to handle thousands of agents
For more examples, see the Examples section and explore the examples/ directory in the repository.