Note
Go to the end to download the full example code.
BAM Operations#
This example provides a comprehensive guide to the ops module,
which offers NumPy-free array operations for writing custom events.
The ops module makes BAM Engine accessible to economists who may not
be familiar with NumPy.
You’ll learn to:
Use arithmetic operations (add, multiply, divide)
Perform comparisons and logical operations
Apply conditional logic (where, select)
Aggregate data (sum, mean, any, all)
Create and manipulate arrays
Use random operations with the simulation RNG
Why Use ops Instead of NumPy?#
The ops module provides:
Safe defaults: Division by zero is handled automatically
Consistent naming: Verb-based names (multiply vs
*)In-place operations: Support for
out=parameterType hints: Better IDE support
Progressive disclosure: Beginners don’t need NumPy knowledge
import numpy as np
from bamengine import ops
# Example: Safe division (no error on division by zero)
prices = np.array([10.0, 20.0, 0.0, 15.0])
quantities = np.array([2.0, 0.0, 5.0, 3.0])
# NumPy would give infinity or NaN for division by zero
# ops.divide handles this safely
unit_price = ops.divide(prices, quantities)
print("Safe division with ops.divide:")
print(f" prices: {prices}")
print(f" quantities: {quantities}")
print(f" unit_price: {unit_price}")
print(" (zeros handled safely)")
Safe division with ops.divide:
prices: [10. 20. 0. 15.]
quantities: [2. 0. 5. 3.]
unit_price: [5.e+00 2.e+11 0.e+00 5.e+00]
(zeros handled safely)
Arithmetic Operations#
Basic math: add, subtract, multiply, divide
a = np.array([1.0, 2.0, 3.0, 4.0])
b = np.array([10.0, 20.0, 30.0, 40.0])
print("\nArithmetic operations:")
print(f" a = {a}")
print(f" b = {b}")
print(f" add(a, b) = {ops.add(a, b)}")
print(f" subtract(b, a) = {ops.subtract(b, a)}")
print(f" multiply(a, 2) = {ops.multiply(a, 2)}")
print(f" divide(b, a) = {ops.divide(b, a)}")
# In-place operations with out parameter
result = np.zeros(4)
ops.add(a, b, out=result)
print(
f" add(a, b, out=result): result is same object? {result is ops.add(a, b, out=result)}"
)
Arithmetic operations:
a = [1. 2. 3. 4.]
b = [10. 20. 30. 40.]
add(a, b) = [11. 22. 33. 44.]
subtract(b, a) = [ 9. 18. 27. 36.]
multiply(a, 2) = [2. 4. 6. 8.]
divide(b, a) = [10. 10. 10. 10.]
add(a, b, out=result): result is same object? True
Comparison Operations#
Element-wise comparisons return boolean arrays.
prices = np.array([0.9, 1.0, 1.1, 1.2])
threshold = 1.0
print("\nComparison operations:")
print(f" prices = {prices}")
print(f" threshold = {threshold}")
print(f" equal(prices, threshold): {ops.equal(prices, threshold)}")
print(f" less(prices, threshold): {ops.less(prices, threshold)}")
print(f" greater(prices, threshold): {ops.greater(prices, threshold)}")
print(f" less_equal(prices, threshold): {ops.less_equal(prices, threshold)}")
print(f" greater_equal(prices, threshold): {ops.greater_equal(prices, threshold)}")
print(f" not_equal(prices, threshold): {ops.not_equal(prices, threshold)}")
Comparison operations:
prices = [0.9 1. 1.1 1.2]
threshold = 1.0
equal(prices, threshold): [False True False False]
less(prices, threshold): [ True False False False]
greater(prices, threshold): [False False True True]
less_equal(prices, threshold): [ True True False False]
greater_equal(prices, threshold): [False True True True]
not_equal(prices, threshold): [ True False True True]
Logical Operations#
Combine boolean conditions.
employed = np.array([True, True, False, True, False])
low_wage = np.array([True, False, True, False, True])
print("\nLogical operations:")
print(f" employed = {employed}")
print(f" low_wage = {low_wage}")
print(f" logical_and(employed, low_wage): {ops.logical_and(employed, low_wage)}")
print(f" logical_or(employed, low_wage): {ops.logical_or(employed, low_wage)}")
print(f" logical_not(employed): {ops.logical_not(employed)}")
Logical operations:
employed = [ True True False True False]
low_wage = [ True False True False True]
logical_and(employed, low_wage): [ True False False False False]
logical_or(employed, low_wage): [ True True True True True]
logical_not(employed): [False False True False True]
Conditional Operations#
Vectorized if-then-else logic.
inventory = np.array([100, 0, 50, 10, 0])
base_price = np.array([10.0, 10.0, 10.0, 10.0, 10.0])
# ops.where: if-then-else
# If inventory > 0, discount by 5%; otherwise premium by 10%
has_inventory = ops.greater(inventory, 0)
new_price = ops.where(
has_inventory,
ops.multiply(base_price, 0.95), # 5% discount
ops.multiply(base_price, 1.10), # 10% premium
)
print("\nConditional with ops.where:")
print(f" inventory: {inventory}")
print(f" has_inventory: {has_inventory}")
print(f" new_price: {new_price}")
print(" (discount if inventory > 0, premium otherwise)")
Conditional with ops.where:
inventory: [100 0 50 10 0]
has_inventory: [ True False True True False]
new_price: [ 9.5 11. 9.5 9.5 11. ]
(discount if inventory > 0, premium otherwise)
Element-wise Operations#
Maximum, minimum, and clipping.
proposed_wages = np.array([0.5, 0.8, 1.2, 0.6, 1.5])
min_wage = 0.7
max_wage = 1.3
print("\nElement-wise operations:")
print(f" proposed_wages: {proposed_wages}")
# Enforce minimum wage
actual_wages = ops.maximum(proposed_wages, min_wage)
print(f" maximum(wages, {min_wage}): {actual_wages}")
# Enforce maximum wage
capped_wages = ops.minimum(proposed_wages, max_wage)
print(f" minimum(wages, {max_wage}): {capped_wages}")
# Enforce both (clip)
bounded_wages = ops.clip(proposed_wages, min_wage, max_wage)
print(f" clip(wages, {min_wage}, {max_wage}): {bounded_wages}")
Element-wise operations:
proposed_wages: [0.5 0.8 1.2 0.6 1.5]
maximum(wages, 0.7): [0.7 0.8 1.2 0.7 1.5]
minimum(wages, 1.3): [0.5 0.8 1.2 0.6 1.3]
clip(wages, 0.7, 1.3): [0.7 0.8 1.2 0.7 1.3]
Aggregation Operations#
Sum, mean, any, all with optional masks.
production = np.array([100.0, 0.0, 50.0, 75.0, 0.0])
prices = np.array([10.0, 0.0, 12.0, 11.0, 0.0])
active = ops.greater(production, 0)
print("\nAggregation operations:")
print(f" production: {production}")
print(f" active: {active}")
# Total production
print(f" sum(production): {ops.sum(production)}")
# Production of active firms only
print(f" sum(production, where=active): {ops.sum(production, where=active)}")
# Average production
print(f" mean(production): {ops.mean(production):.2f}")
# Average of active firms
print(f" mean(production, where=active): {ops.mean(production, where=active):.2f}")
# Check if any firm is inactive
inactive = ops.logical_not(active)
print(f" any(inactive): {ops.any(inactive)}")
# Check if all firms are active
print(f" all(active): {ops.all(active)}")
Aggregation operations:
production: [100. 0. 50. 75. 0.]
active: [ True False True True False]
sum(production): 225.0
sum(production, where=active): 225.0
mean(production): 45.00
mean(production, where=active): 75.00
any(inactive): True
all(active): False
Array Creation#
Create arrays of specific values.
n = 5
print("\nArray creation:")
print(f" zeros({n}): {ops.zeros(n)}")
print(f" ones({n}): {ops.ones(n)}")
print(f" full({n}, 3.14): {ops.full(n, 3.14)}")
print(f" empty({n}): (uninitialized values)")
# arange: create evenly spaced values
periods = ops.arange(0, 5, 1)
print(f" arange(0, 5, 1): {periods}")
# arange with fractional step
time_axis = ops.arange(0, 1, 0.25)
print(f" arange(0, 1, 0.25): {time_axis}")
# array: create array from Python list (always copies)
data_list = [1.0, 2.0, 3.0, 4.0]
arr = ops.array(data_list)
print(f"\n array({data_list}): {arr}")
# array always creates a copy, even from existing arrays
original = np.array([10.0, 20.0, 30.0])
copy = ops.array(original)
copy[0] = 999 # Modifying copy
print(f" array() creates copy: original={original}, copy={copy}")
# asarray: creates view when possible (no copy for compatible arrays)
view = ops.asarray(original)
print(f" asarray() may create view: shares memory? {np.shares_memory(original, view)}")
# Key difference: array always copies, asarray may reuse memory
# Use array when you need a fresh copy
# Use asarray when you want to avoid unnecessary copies
Array creation:
zeros(5): [0. 0. 0. 0. 0.]
ones(5): [1. 1. 1. 1. 1.]
full(5, 3.14): [3.14 3.14 3.14 3.14 3.14]
empty(5): (uninitialized values)
arange(0, 5, 1): [0. 1. 2. 3. 4.]
arange(0, 1, 0.25): [0. 0.25 0.5 0.75]
array([1.0, 2.0, 3.0, 4.0]): [1. 2. 3. 4.]
array() creates copy: original=[10. 20. 30.], copy=[999. 20. 30.]
asarray() may create view: shares memory? True
Mathematical Functions#
Logarithm, exponential, and other math operations.
gdp = np.array([100.0, 110.0, 121.0, 133.1])
print("\nMathematical functions:")
print(f" gdp: {gdp}")
# Natural logarithm (useful for log-scale analysis)
log_gdp = ops.log(gdp)
print(f" log(gdp): {log_gdp.round(3)}")
# Log differences approximate growth rates
log_diff = np.diff(log_gdp)
print(f" log differences (≈ growth rates): {log_diff.round(3)}")
# Exponential function (e^x)
# Useful for reversing log transform or modeling exponential decay
growth_rates = np.array([0.0, 0.05, 0.10, -0.05])
print(f"\n growth_rates: {growth_rates}")
growth_factors = ops.exp(growth_rates)
print(f" exp(growth_rates): {growth_factors.round(4)}")
# Common pattern: calculate probability-like decays
# sigma = 0.1 * exp(-fragility), where higher fragility reduces investment
fragility = np.array([0.0, 0.5, 1.0, 2.0])
investment_share = ops.multiply(0.1, ops.exp(ops.multiply(-1.0, fragility)))
print(f"\n fragility: {fragility}")
print(f" 0.1 * exp(-fragility): {investment_share.round(4)}")
print(" (higher fragility -> lower investment)")
Mathematical functions:
gdp: [100. 110. 121. 133.1]
log(gdp): [4.605 4.7 4.796 4.891]
log differences (≈ growth rates): [0.095 0.095 0.095]
growth_rates: [ 0. 0.05 0.1 -0.05]
exp(growth_rates): [1. 1.0513 1.1052 0.9512]
fragility: [0. 0.5 1. 2. ]
0.1 * exp(-fragility): [0.1 0.0607 0.0368 0.0135]
(higher fragility -> lower investment)
Utility Operations#
Sorting, unique values, counting, membership.
employer_ids = np.array([0, 2, 1, 0, 2, 2, 1, 0])
print("\nUtility operations:")
print(f" employer_ids: {employer_ids}")
# Unique employers
print(f" unique(employer_ids): {ops.unique(employer_ids)}")
# Count workers per employer
counts = ops.bincount(employer_ids, minlength=3)
print(f" bincount (workers per employer): {counts}")
# Check membership
target_employers = np.array([0, 1])
is_target = ops.isin(employer_ids, target_employers)
print(f" isin(ids, [0,1]): {is_target}")
# Sorting
prices = np.array([30.0, 10.0, 25.0, 15.0])
print(f"\n prices: {prices}")
print(f" sort(prices): {ops.sort(prices)}")
print(f" argsort(prices): {ops.argsort(prices)}")
print(" (argsort gives indices that would sort the array)")
Utility operations:
employer_ids: [0 2 1 0 2 2 1 0]
unique(employer_ids): [0 1 2]
bincount (workers per employer): [3 2 3]
isin(ids, [0,1]): [ True False True True False False True True]
prices: [30. 10. 25. 15.]
sort(prices): [10. 15. 25. 30.]
argsort(prices): [1 3 2 0]
(argsort gives indices that would sort the array)
In-Place Assignment#
Modify arrays in place with ops.assign.
prices = np.array([10.0, 20.0, 30.0])
print("\nIn-place assignment:")
print(f" Before: {prices}")
# Calculate new prices
new_prices = ops.multiply(prices, 1.1)
# Assign in place (equivalent to prices[:] = new_prices)
ops.assign(prices, new_prices)
print(f" After ops.assign: {prices}")
In-place assignment:
Before: [10. 20. 30.]
After ops.assign: [11. 22. 33.]
Random Operations#
Generate random numbers using simulation RNG.
import bamengine as bam
# Create simulation to get RNG
sim = bam.Simulation.init(n_firms=100, n_households=500, seed=42)
# Generate uniform random numbers
shocks = ops.uniform(sim.rng, low=-0.1, high=0.1, size=10)
print("\nRandom operations:")
print(" uniform(rng, -0.1, 0.1, size=10):")
print(f" {shocks.round(4)}")
# Random shocks are reproducible with same seed
sim2 = bam.Simulation.init(n_firms=100, n_households=500, seed=42)
shocks2 = ops.uniform(sim2.rng, low=-0.1, high=0.1, size=10)
print(f" Same seed produces same values: {np.allclose(shocks, shocks2)}")
Random operations:
uniform(rng, -0.1, 0.1, size=10):
[ 0.0548 -0.0122 0.0717 0.0395 -0.0812 0.0951 0.0522 0.0572 -0.0744
-0.0099]
Same seed produces same values: True
Practical Example: Custom Pricing Event#
Combine ops operations in a realistic custom event.
from bamengine import event
@event
class CostPlusPricing:
"""Set prices using cost-plus markup strategy.
This event demonstrates using multiple ops operations together
in a realistic economic mechanism.
"""
def execute(self, sim):
# Get roles
prod = sim.get_role("Producer")
emp = sim.get_role("Employer")
# Parameters
markup_rate = 0.20 # 20% markup
price_floor = 0.5 # Minimum price
max_price_change = 0.10 # Max 10% change per period
# Calculate unit labor cost
# (wage / productivity, safe division handles zeros)
unit_cost = ops.divide(emp.wage_offer, prod.labor_productivity)
# Apply markup
target_price = ops.multiply(unit_cost, 1.0 + markup_rate)
# Enforce price floor
target_price = ops.maximum(target_price, price_floor)
# Limit price changes (gradual adjustment)
price_change = ops.subtract(target_price, prod.price)
max_increase = ops.multiply(prod.price, max_price_change)
max_decrease = ops.multiply(prod.price, -max_price_change)
# Clip change to allowed range
bounded_change = ops.clip(price_change, max_decrease, max_increase)
# Apply bounded change
new_price = ops.add(prod.price, bounded_change)
# Update prices in place
ops.assign(prod.price, new_price)
print("\nCostPlusPricing event defined")
print("Operations used:")
print(" - divide (safe division for unit cost)")
print(" - multiply (markup application)")
print(" - maximum (price floor)")
print(" - subtract (price change)")
print(" - clip (bound changes)")
print(" - add (apply change)")
print(" - assign (in-place update)")
# Test the event
sim = bam.Simulation.init(n_firms=50, n_households=250, seed=42)
sim.run(n_periods=5) # Establish some state
print(f"\nBefore CostPlusPricing: Mean price = {np.mean(sim.prod.price):.3f}")
pricing_event = CostPlusPricing()
pricing_event.execute(sim)
print(f"After CostPlusPricing: Mean price = {np.mean(sim.prod.price):.3f}")
CostPlusPricing event defined
Operations used:
- divide (safe division for unit cost)
- multiply (markup application)
- maximum (price floor)
- subtract (price change)
- clip (bound changes)
- add (apply change)
- assign (in-place update)
Before CostPlusPricing: Mean price = 0.501
After CostPlusPricing: Mean price = 0.501
Visualization: Ops in Action#
Visualize how ops operations affect simulation dynamics.
import matplotlib.pyplot as plt
# Simulate with and without price floor
n_periods = 50
# Standard simulation
sim_standard = bam.Simulation.init(n_firms=100, n_households=500, seed=42)
prices_standard = []
for _ in range(n_periods):
sim_standard.step()
prices_standard.append(np.mean(sim_standard.prod.price))
# Simulation with price floor applied each period
@event
class ApplyPriceFloor:
def execute(self, sim):
prod = sim.get_role("Producer")
floor = 0.8
ops.assign(prod.price, ops.maximum(prod.price, floor))
sim_floor = bam.Simulation.init(n_firms=100, n_households=500, seed=42)
floor_event = ApplyPriceFloor()
prices_floor = []
for _ in range(n_periods):
sim_floor.step()
floor_event.execute(sim_floor)
prices_floor.append(np.mean(sim_floor.prod.price))
# Plot comparison
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(prices_standard, label="Standard", linewidth=2)
ax.plot(prices_floor, label="With Price Floor (0.8)", linewidth=2)
ax.axhline(y=0.8, color="red", linestyle="--", alpha=0.5, label="Floor")
ax.set_xlabel("Period")
ax.set_ylabel("Mean Price")
ax.set_title("Effect of ops.maximum Price Floor")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Operations Reference#
Quick reference of all available operations.
reference = """
ARITHMETIC
add(a, b) - Element-wise addition
subtract(a, b) - Element-wise subtraction
multiply(a, b) - Element-wise multiplication
divide(a, b) - Safe division (handles zeros)
ASSIGNMENT
assign(target, val) - In-place assignment (target[:] = val)
COMPARISON
equal(a, b) - Element-wise ==
not_equal(a, b) - Element-wise !=
less(a, b) - Element-wise <
less_equal(a, b) - Element-wise <=
greater(a, b) - Element-wise >
greater_equal(a, b) - Element-wise >=
LOGICAL
logical_and(a, b) - Element-wise AND
logical_or(a, b) - Element-wise OR
logical_not(a) - Element-wise NOT
CONDITIONAL
where(cond, x, y) - If-then-else
select(conds, vals) - Multi-condition switch
ELEMENT-WISE
maximum(a, b) - Element-wise max
minimum(a, b) - Element-wise min
clip(a, lo, hi) - Clip to range [lo, hi]
AGGREGATION
sum(a, axis, where) - Sum elements
mean(a, axis, where)- Mean of elements
any(a) - Any True?
all(a) - All True?
ARRAY CREATION
zeros(n) - Array of zeros
ones(n) - Array of ones
full(n, val) - Array of constant value
empty(n) - Uninitialized array
arange(start, stop) - Evenly spaced values
array(data) - Create array (always copies)
asarray(data) - Convert to array (may share memory)
MATHEMATICAL
log(a) - Natural logarithm
exp(a) - Exponential (e^x)
UTILITIES
unique(a) - Unique sorted values
bincount(a) - Count occurrences
isin(a, vals) - Membership test
argsort(a) - Indices that sort array
sort(a) - Sorted copy
RANDOM
uniform(rng, lo, hi, size) - Uniform random numbers
"""
print(reference)
ARITHMETIC
add(a, b) - Element-wise addition
subtract(a, b) - Element-wise subtraction
multiply(a, b) - Element-wise multiplication
divide(a, b) - Safe division (handles zeros)
ASSIGNMENT
assign(target, val) - In-place assignment (target[:] = val)
COMPARISON
equal(a, b) - Element-wise ==
not_equal(a, b) - Element-wise !=
less(a, b) - Element-wise <
less_equal(a, b) - Element-wise <=
greater(a, b) - Element-wise >
greater_equal(a, b) - Element-wise >=
LOGICAL
logical_and(a, b) - Element-wise AND
logical_or(a, b) - Element-wise OR
logical_not(a) - Element-wise NOT
CONDITIONAL
where(cond, x, y) - If-then-else
select(conds, vals) - Multi-condition switch
ELEMENT-WISE
maximum(a, b) - Element-wise max
minimum(a, b) - Element-wise min
clip(a, lo, hi) - Clip to range [lo, hi]
AGGREGATION
sum(a, axis, where) - Sum elements
mean(a, axis, where)- Mean of elements
any(a) - Any True?
all(a) - All True?
ARRAY CREATION
zeros(n) - Array of zeros
ones(n) - Array of ones
full(n, val) - Array of constant value
empty(n) - Uninitialized array
arange(start, stop) - Evenly spaced values
array(data) - Create array (always copies)
asarray(data) - Convert to array (may share memory)
MATHEMATICAL
log(a) - Natural logarithm
exp(a) - Exponential (e^x)
UTILITIES
unique(a) - Unique sorted values
bincount(a) - Count occurrences
isin(a, vals) - Membership test
argsort(a) - Indices that sort array
sort(a) - Sorted copy
RANDOM
uniform(rng, lo, hi, size) - Uniform random numbers
Key Takeaways#
Use
opsfor NumPy-free array operationsops.dividehandles division by zero safelyops.wherefor if-then-else,ops.selectfor switch/caseops.sum/meansupportwhereparameter for filtered aggregationops.assignfor in-place array updatesops.uniformfor random numbers (requiressim.rng)ops.expfor exponential decay patterns (e.g., R&D investment)ops.arrayfor creating copies,ops.asarrayfor memory-efficient viewsAll operations can be combined for complex economic logic
Total running time of the script: (0 minutes 0.657 seconds)