Custom Relationships#
Relationships represent many-to-many connections between agents with per-edge data. They are stored in a sparse COO (Coordinate) format for memory efficiency, since most agent pairs are not connected at any given time.
Quick Example#
from bamengine import relationship, get_role
from bamengine.typing import Float, Int
@relationship(source=get_role("Worker"), target=get_role("Employer"))
class Employment:
"""Employment relationship between workers and firms."""
wage: Float
contract_duration: Int
start_period: Int
The @relationship Decorator#
The decorator defines a relationship type with source/target roles and per-edge data fields:
@relationship(
source=get_role("Borrower"), # Source agent role
target=get_role("Lender"), # Target agent role
cardinality="many-to-many", # Default
name="CustomLoanBook", # Optional name override
)
class CustomLoanBook:
principal: Float
rate: Float
Parameters:
source: The role type on the “from” side of each edge
target: The role type on the “to” side of each edge
cardinality:
"many-to-many"(default),"one-to-many", or"many-to-one"name: Custom name for registry lookup (defaults to class name)
The decorator registers the relationship in the global registry and converts edge data fields into parallel arrays.
COO Sparse Format#
Relationships store edges as parallel arrays in COO (Coordinate) format:
Index: 0 1 2 3 4
source_ids: [3, 7, 3, 12, 7 ] ← borrower firm IDs
target_ids: [0, 2, 1, 0, 0 ] ← lender bank IDs
principal: [10.5, 25.0, 8.3, 15.0, 12.0 ] ← per-edge data
rate: [0.03, 0.02, 0.04, 0.03, 0.02 ] ← per-edge data
Key attributes:
source_ids: array of source agent IDstarget_ids: array of target agent IDssize: number of active edges (valid entries in the arrays)capacity: allocated array length (grows dynamically via doubling)
Only indices 0 through size - 1 contain valid data.
Adding and Removing Edges#
Adding edges:
import numpy as np
# Append edges with source/target IDs and component data
rel.append_edges(
source_ids=np.array([0, 1, 2]),
target_ids=np.array([5, 5, 3]),
principal=np.array([10.0, 20.0, 15.0]),
rate=np.array([0.02, 0.03, 0.025]),
)
The LoanBook has a convenience method for appending loans from a single
lender:
loans.append_loans_for_lender(
lender_idx=np.intp(0),
borrower_indices=np.array([3, 7]),
amount=np.array([10.5, 25.0]),
rate=np.array([0.03, 0.02]),
)
Removing edges:
# Remove by boolean mask (True = remove)
mask = rel.principal < 1.0 # Remove tiny loans
rel.drop_rows(mask[: rel.size])
# Remove all edges from specific sources
rel.purge_sources(np.array([3, 12])) # Remove all loans from firms 3 and 12
# Remove all edges to specific targets
rel.purge_targets(np.array([0])) # Remove all loans to bank 0
# Clear all edges
rel.size = 0
Querying Edges#
Find edges by source or target agent:
# Get indices of all loans from firm 7
edge_indices = loans.query_sources(7)
firm_7_rates = loans.rate[edge_indices]
# Get indices of all loans to bank 0
edge_indices = loans.query_targets(0)
bank_0_principal = loans.principal[edge_indices]
Aggregating Edge Data#
Compute per-agent aggregates from edge data:
# Total debt per borrower (firm)
debt_by_firm = loans.aggregate_by_source(
"debt",
func="sum",
n_sources=sim.n_firms,
)
# Total lending per lender (bank)
lending_by_bank = loans.aggregate_by_target(
"principal",
func="sum",
n_targets=sim.n_banks,
)
# Average interest rate per bank
avg_rate_by_bank = loans.aggregate_by_target(
"rate",
func="mean",
n_targets=sim.n_banks,
)
# Number of loans per firm
loan_count_by_firm = loans.aggregate_by_source(
"principal",
func="count",
n_sources=sim.n_firms,
)
Available aggregation functions: "sum", "mean", "count",
"min", "max".
Built-in Relationship: LoanBook#
The LoanBook relationship tracks
loans between firms (borrowers) and banks (lenders):
Field |
Description |
|---|---|
|
Original loan amount at signing |
|
Contractual interest rate |
|
Cached interest amount ( |
|
Cached total debt ( |
|
Borrower (firm) IDs (also accessible as |
|
Lender (bank) IDs (also accessible as |
Convenience methods:
loans = sim.lb # shortcut for sim.get_relationship("LoanBook")
loans.debt_per_borrower(n_borrowers=sim.n_firms) # Total debt per firm
loans.interest_per_borrower(n_borrowers=sim.n_firms) # Total interest per firm
loans.principal_per_borrower(n_borrowers=sim.n_firms) # Total principal per firm
Tips#
Edge indices are NOT stable: Adding or removing edges may shift indices. Don’t store edge indices across operations.
Use ``size``, not ``len()``: Only
relationship.sizeentries are valid. Arrays may have extra capacity beyondsize.COO is efficient for sparse, dynamic graphs: The format excels when most agent pairs are unconnected and the set of connections changes each period.
Aggregation is vectorized:
aggregate_by_sourceandaggregate_by_targetusenp.bincountinternally, so they’re fast even with thousands of edges.
See also
Data Collection for collecting relationship data during runs
Custom Events for using relationships in event implementations
LoanBookAPI reference