LoanBook#
- class bamengine.relationships.loanbook.LoanBook(source_ids=<factory>, target_ids=<factory>, size=0, capacity=128, principal=<factory>, rate=<factory>, interest=<factory>, debt=<factory>)[source]#
Bases:
RelationshipSparse edge-list ledger for managing active loan contracts.
LoanBook is a Relationship between Borrower (source/firms) and Lender (target/banks), storing loan contracts in COO (Coordinate List) sparse format. This avoids the memory overhead of a dense (n_firms × n_banks) matrix while enabling efficient vectorized operations.
Inherits from Relationship base class, which provides:
source_ids (borrower indices)
target_ids (lender indices)
size (number of active loans)
capacity (allocated storage)
Query methods (query_sources, query_targets)
Aggregation methods (aggregate_by_source, aggregate_by_target)
Deletion methods (drop_rows, purge_sources, purge_targets)
- Parameters:
principal (
Float1D) – Loan principal amounts (original loan amounts at signing).rate (
Float1D) – Contractual interest rates for each loan.interest (
Float1D) – Cached interest amounts (rate × principal), enables O(1) aggregation.debt (
Float1D) – Cached total debt (principal × (1 + rate)), enables O(1) aggregation.source_ids (
Idx1D) – Borrower (firm) IDs for each loan.target_ids (
Idx1D) – Lender (bank) IDs for each loan.size (
int) – Number of active loans (valid entries in arrays).capacity (
int) – Allocated array size (grows via doubling when exceeded).
Examples
Access from simulation:
>>> import bamengine as be >>> sim = be.Simulation.init(n_firms=100, n_banks=10, seed=42) >>> loans = sim.loans >>> loans.size 45
Append new loans from bank 0 to firms 1, 2, 3:
>>> import numpy as np >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 2, 3]), ... amount=np.array([100.0, 150.0, 200.0]), ... rate=np.array([0.02, 0.03, 0.02]), ... )
Query loans for specific borrower:
>>> borrower_id = 5 >>> loan_mask = loans.source_ids[: loans.size] == borrower_id >>> borrower_loans = loans.principal[: loans.size][loan_mask] >>> borrower_loans.sum() 250.0
Aggregate total debt per borrower:
>>> total_debt = loans.debt_per_borrower(n_borrowers=100) >>> total_debt.shape (100,) >>> total_debt[5] 255.0
Aggregate total interest per borrower:
>>> total_interest = loans.interest_per_borrower(n_borrowers=100) >>> total_interest[5] 5.0
Purge loans from bankrupt firms:
>>> bankrupt = np.array([1, 7, 12]) >>> removed = loans.purge_borrowers(bankrupt) >>> removed 3
Notes
Memory Efficiency: For 100 firms, 10 banks, and 50 active loans:
Dense matrix: 100 × 10 × 4 fields × 8 bytes = 32,000 bytes
Sparse COO: 50 × 6 arrays × 8 bytes = 2,400 bytes (~13x smaller)
Performance: Aggregation operations use vectorized NumPy primitives:
debt_per_borrower: O(size) using np.add.at
purge_borrowers: O(size) using np.isin and boolean indexing
append_loans: O(1) amortized via doubling strategy
Backward Compatibility: The borrower and lender properties provide aliases for source_ids and target_ids to maintain compatibility with existing code that predates the Relationship abstraction.
See also
RelationshipBase class with query/aggregation
BorrowerSource role (firms seeking credit)
LenderTarget role (banks providing credit)
credit_marketLoan creation logic
revenueDebt repayment logic
- principal#
Loan principal amounts (original loan amounts at signing).
- rate#
Contractual interest rates for each loan.
- interest#
Cached interest amounts (rate * principal).
- debt#
Cached total debt (principal * (1 + rate)).
- property borrower#
Alias for source_ids (borrower firm indices).
Provides backward compatibility with code written before the Relationship abstraction. New code should use source_ids directly.
- Returns:
Array of borrower indices (same as source_ids).
- Return type:
Int1D
Examples
>>> from bamengine.relationships import LoanBook >>> loans = LoanBook() >>> loans.borrower is loans.source_ids True
- __init__(source_ids=<factory>, target_ids=<factory>, size=0, capacity=128, principal=<factory>, rate=<factory>, interest=<factory>, debt=<factory>)#
- cardinality = 'many-to-many'#
- property lender#
Alias for target_ids (lender bank indices).
Provides backward compatibility with code written before the Relationship abstraction. New code should use target_ids directly.
- Returns:
Array of lender indices (same as target_ids).
- Return type:
Int1D
Examples
>>> from bamengine.relationships import LoanBook >>> loans = LoanBook() >>> loans.lender is loans.target_ids True
- name = 'LoanBook'#
- debt_per_borrower(n_borrowers)[source]#
Aggregate total debt per borrower using vectorized summation.
Uses the inherited aggregate_by_source method from Relationship base class, which employs np.add.at for efficient aggregation.
- Parameters:
n_borrowers (
int) – Number of borrowers in the simulation (typically n_firms).- Returns:
Array of shape (n_borrowers,) containing total debt per borrower. Borrowers with no loans have debt = 0.0.
- Return type:
Float1D
Examples
>>> from bamengine.relationships import LoanBook >>> import numpy as np >>> loans = LoanBook() >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 1, 3]), ... amount=np.array([100.0, 50.0, 200.0]), ... rate=np.array([0.02, 0.03, 0.02]), ... ) >>> debt = loans.debt_per_borrower(n_borrowers=5) >>> debt.shape (5,) >>> debt[1] 154.5 >>> debt[3] 204.0
See also
interest_per_borrowerAggregate interest per borrower
bamengine.core.relationship.Relationship.aggregate_by_sourceBase method
- interest_per_borrower(n_borrowers)[source]#
Aggregate total interest per borrower using vectorized summation.
Uses the inherited aggregate_by_source method from Relationship base class, which employs np.add.at for efficient aggregation.
- Parameters:
n_borrowers (
int) – Number of borrowers in the simulation (typically n_firms).- Returns:
Array of shape (n_borrowers,) containing total interest per borrower. Borrowers with no loans have interest = 0.0.
- Return type:
Float1D
Examples
>>> from bamengine.relationships import LoanBook >>> import numpy as np >>> loans = LoanBook() >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 1, 3]), ... amount=np.array([100.0, 50.0, 200.0]), ... rate=np.array([0.02, 0.03, 0.02]), ... ) >>> interest = loans.interest_per_borrower(n_borrowers=5) >>> interest.shape (5,) >>> interest[1] 3.5 >>> interest[3] 4.0
See also
debt_per_borrowerAggregate debt per borrower
bamengine.core.relationship.Relationship.aggregate_by_sourceBase method
- principal_per_borrower(n_borrowers)[source]#
Aggregate total principal per borrower using vectorized summation.
Uses the inherited aggregate_by_source method from Relationship base class, which employs np.add.at for efficient aggregation.
- Parameters:
n_borrowers (
int) – Number of borrowers in the simulation (typically n_firms).- Returns:
Array of shape (n_borrowers,) containing total principal per borrower. Borrowers with no loans have principal = 0.0.
- Return type:
Float1D
Examples
>>> from bamengine.relationships import LoanBook >>> import numpy as np >>> loans = LoanBook() >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 1, 3]), ... amount=np.array([100.0, 50.0, 200.0]), ... rate=np.array([0.02, 0.03, 0.02]), ... ) >>> principal = loans.principal_per_borrower(n_borrowers=5) >>> principal.shape (5,) >>> principal[1] 150.0 >>> principal[3] 200.0
See also
debt_per_borrowerAggregate debt per borrower
interest_per_borrowerAggregate interest per borrower
bamengine.core.relationship.Relationship.aggregate_by_sourceBase method
- append_loans_for_lender(lender_idx, borrower_indices, amount, rate)[source]#
Append new loans from a specific lender to multiple borrowers.
Automatically resizes arrays if needed using doubling strategy. Caches interest and debt for O(1) aggregation later.
- Parameters:
lender_idx (
np.intp) – Index of the lender providing loans (bank ID).borrower_indices (
Idx1D) – Indices of borrowers receiving loans (firm IDs).amount (
Float1D) – Principal amounts for each loan.rate (
Float1D) – Interest rates for each loan.
Notes
This method:
Ensures capacity via _ensure_capacity (may trigger resize)
Appends source_ids (borrowers), target_ids (lender)
Appends principal and rate
Caches interest = amount × rate
Caches debt = amount × (1 + rate)
Updates size counter
The lender_idx is broadcast to all new loan entries (scalar expansion).
Examples
>>> from bamengine.relationships import LoanBook >>> import numpy as np >>> loans = LoanBook() >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 2, 3]), ... amount=np.array([100.0, 150.0, 200.0]), ... rate=np.array([0.02, 0.03, 0.02]), ... ) >>> loans.size 3 >>> loans.source_ids[:3] array([1, 2, 3]) >>> loans.target_ids[:3] array([0, 0, 0]) >>> loans.principal[:3] array([100., 150., 200.])
See also
_ensure_capacityInternal resize method
- append_loans_batch(lender_indices, borrower_indices, amount, rate)[source]#
Append loans from multiple lenders in a single batch.
Unlike
append_loans_for_lender()which takes a scalar lender, this method takes an array of lender IDs — one per loan. Used by the vectorized credit market to write all loans from one round at once.- Parameters:
lender_indices (
Idx1D) – Lender (bank) ID for each loan.borrower_indices (
Idx1D) – Borrower (firm) ID for each loan.amount (
Float1D) – Principal amounts.rate (
Float1D) – Interest rates.
- drop_rows(rows_mask)[source]#
Remove loans matching a boolean mask and compact arrays in-place.
Overrides Relationship.drop_rows() to also compact loan-specific component arrays (principal, rate, interest, debt).
- Parameters:
rows_mask (
Bool1D) – Boolean mask over active loans (length >= size). True → loan will be removed, False → loan kept.- Returns:
Number of loans removed.
- Return type:
Notes
This method:
Inverts mask to get loans to keep
Compacts all six arrays (source_ids, target_ids, principal, rate, interest, debt)
Updates size counter
Returns number of removed loans
The compaction is done in-place using boolean indexing, which is cache-friendly and avoids temporary array allocations.
Examples
>>> from bamengine.relationships import LoanBook >>> import numpy as np >>> loans = LoanBook() >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 2, 3, 4]), ... amount=np.array([100.0, 150.0, 200.0, 250.0]), ... rate=np.array([0.02, 0.03, 0.02, 0.03]), ... ) >>> loans.size 4 >>> # Remove loans with principal > 150 >>> mask = loans.principal[: loans.size] > 150.0 >>> removed = loans.drop_rows(mask) >>> removed 2 >>> loans.size 2
See also
purge_borrowersRemove loans by borrower IDs
purge_lendersRemove loans by lender IDs
- purge_borrowers(borrower_ids)[source]#
Remove all loans from specified borrowers (firms).
Uses inherited purge_sources() method from Relationship base class, which internally uses np.isin for efficient matching and drop_rows for compaction.
- Parameters:
borrower_ids (
Idx1D) – Array of borrower (firm) indices to purge.- Returns:
Number of loans removed.
- Return type:
Notes
This is typically called during bankruptcy resolution to remove all loans from insolvent firms.
Time complexity: O(size) for np.isin + O(size) for compaction = O(size).
Examples
>>> from bamengine.relationships import LoanBook >>> import numpy as np >>> loans = LoanBook() >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 2, 3, 4, 5]), ... amount=np.array([100.0, 150.0, 200.0, 250.0, 300.0]), ... rate=np.array([0.02, 0.03, 0.02, 0.03, 0.02]), ... ) >>> loans.size 5 >>> # Purge bankrupt firms 2 and 4 >>> removed = loans.purge_borrowers(np.array([2, 4])) >>> removed 2 >>> loans.size 3 >>> np.sort(loans.source_ids[: loans.size]) array([1, 3, 5])
See also
purge_lendersRemove loans by lender IDs
drop_rowsRemove loans by boolean mask
bamengine.core.relationship.Relationship.purge_sourcesBase method
- purge_lenders(lender_ids)[source]#
Remove all loans from specified lenders (banks).
Uses inherited purge_targets() method from Relationship base class, which internally uses np.isin for efficient matching and drop_rows for compaction.
- Parameters:
lender_ids (
Idx1D) – Array of lender (bank) indices to purge.- Returns:
Number of loans removed.
- Return type:
Notes
This is typically called during bank bankruptcy resolution to remove all loans from insolvent banks.
Time complexity: O(size) for np.isin + O(size) for compaction = O(size).
Examples
>>> from bamengine.relationships import LoanBook >>> import numpy as np >>> loans = LoanBook() >>> # Add loans from two different banks >>> loans.append_loans_for_lender( ... lender_idx=0, ... borrower_indices=np.array([1, 2]), ... amount=np.array([100.0, 150.0]), ... rate=np.array([0.02, 0.03]), ... ) >>> loans.append_loans_for_lender( ... lender_idx=1, ... borrower_indices=np.array([3, 4]), ... amount=np.array([200.0, 250.0]), ... rate=np.array([0.02, 0.03]), ... ) >>> loans.size 4 >>> # Purge bankrupt bank 0 >>> removed = loans.purge_lenders(np.array([0])) >>> removed 2 >>> loans.size 2 >>> loans.target_ids[: loans.size] array([1, 1])
See also
purge_borrowersRemove loans by borrower IDs
drop_rowsRemove loans by boolean mask
bamengine.core.relationship.Relationship.purge_targetsBase method