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: Relationship

Sparse 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

Relationship

Base class with query/aggregation

Borrower

Source role (firms seeking credit)

Lender

Target role (banks providing credit)

credit_market

Loan creation logic

revenue

Debt 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'#
source_role#

alias of Borrower

target_role#

alias of Lender

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_borrower

Aggregate interest per borrower

bamengine.core.relationship.Relationship.aggregate_by_source

Base 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
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_borrower

Aggregate debt per borrower

interest_per_borrower

Aggregate interest per borrower

bamengine.core.relationship.Relationship.aggregate_by_source

Base 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:

  1. Ensures capacity via _ensure_capacity (may trigger resize)

  2. Appends source_ids (borrowers), target_ids (lender)

  3. Appends principal and rate

  4. Caches interest = amount × rate

  5. Caches debt = amount × (1 + rate)

  6. 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_capacity

Internal 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:

int

Notes

This method:

  1. Inverts mask to get loans to keep

  2. Compacts all six arrays (source_ids, target_ids, principal, rate, interest, debt)

  3. Updates size counter

  4. 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_borrowers

Remove loans by borrower IDs

purge_lenders

Remove 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:

int

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_lenders

Remove loans by lender IDs

drop_rows

Remove loans by boolean mask

bamengine.core.relationship.Relationship.purge_sources

Base 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:

int

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_borrowers

Remove loans by borrower IDs

drop_rows

Remove loans by boolean mask

bamengine.core.relationship.Relationship.purge_targets

Base method