Individual-Based Model (IBM) Example¶
This guide demonstrates how to attach an IBM group to an Ecosim simulation. The IBM replaces the standard ODE derivative for one or more functional groups, while keeping the rest of the food web coupled via the normal Ecosim solver.
For parameter tuning advice, see the IBM Parameterization Guide.
Quick Start: Ecosim + SmeltIBM¶
from pypath import (
create_rpath_params, rpath, rsim_scenario, rsim_run,
SmeltIBM, SmeltParams,
)
# 1. Build and balance a model (abbreviated)
params = create_rpath_params(
groups=["Phytoplankton", "Zooplankton", "Smelt", "Cod", "Detritus"],
types=[1, 0, 0, 0, 2],
)
# ... fill in model table and diet matrix ...
model = rpath(params)
# 2. Create Ecosim scenario
scenario = rsim_scenario(model, params, years=range(1, 51))
# 3. Set up the IBM
ng = scenario.params.NUM_GROUPS + 1
smelt = SmeltIBM(
group_index=3, # Smelt is group 3 in this model
n_groups=ng,
params=SmeltParams.baltic_defaults(),
)
# 4. Initialize population from Ecopath biomass
smelt.initialize_from_ecosim(
biomass=scenario.params.BB[3],
params={},
n_super_individuals=500,
)
# 5. Register and run
scenario.params.ibm_groups = {3: smelt}
output = rsim_run(scenario)
How It Works¶
When ibm_groups is set on the scenario, the Ecosim derivative
function (deriv_vector) automatically:
- Skips IBM groups in the standard consumption kernel
- Calls
compute_step()on each IBM group - Overrides the derivative:
deriv[group_idx] = (ibm_biomass - BB[group_idx]) / dt - Subtracts IBM consumption from prey derivatives
The rest of the food web runs normally through the ODE solver.
SmeltIBM 5-Phase Timestep¶
Each call to compute_step() executes:
| Phase | Module | Action |
|---|---|---|
| 1. Forage + Grow | bioenergetics, behavior |
Adaptive prey selection, Wisconsin growth |
| 2. Reproduce | reproduction |
Spawning, Cushing larval survival |
| 3. Predation | predation |
Size-structured mortality |
| 4. Bookkeeping | -- | Aging, senescence removal, recruit addition |
| 5. Movement | behavior |
Spatial patch selection (Ecospace only) |
Multiple IBM Groups¶
Multiple species can be IBM-managed simultaneously:
smelt = SmeltIBM(group_index=3, n_groups=ng, params=SmeltParams.baltic_defaults())
smelt.initialize_from_ecosim(scenario.params.BB[3], {}, 500)
herring = SmeltIBM(group_index=4, n_groups=ng, params=SmeltParams.baltic_defaults())
herring.initialize_from_ecosim(scenario.params.BB[4], {}, 300)
scenario.params.ibm_groups = {3: smelt, 4: herring}
output = rsim_run(scenario)
Each IBM group is processed independently. They share the same prey biomass pool, so consumption by one group affects prey available to the other.
Customizing SmeltParams¶
Override individual parameter sub-modules while keeping defaults for the rest:
from pypath.ibm.bioenergetics import BioenergParams
from pypath.ibm.smelt import SmeltParams
# Start from defaults
sp = SmeltParams.baltic_defaults()
# Override just bioenergetics for a warmer-water species
sp.bioenerg = BioenergParams(
ra=0.0040,
rb=-0.25,
q10=2.5,
t_ref=15.0,
sda_fraction=0.15,
unassimilated_fraction=0.22,
a_length=0.48,
b_length=0.333,
energy_density=6.0,
reproduction_fraction=0.25,
)
# Adjust growth parameters
sp.vbgf_linf_mean = 30.0 # larger asymptotic length
sp.max_age = 12.0 # longer-lived
ibm = SmeltIBM(group_index=3, n_groups=ng, params=sp)
Creating a Custom IBM Species¶
Subclass IBMGroup and implement four methods:
from pypath.ibm.base import IBMGroup, IBMStepResult, SpatialContext
import numpy as np
class CustomIBM(IBMGroup):
def __init__(self, group_index, n_groups, my_params):
super().__init__(group_index, n_groups)
self.my_params = my_params
self.individuals = []
def initialize_from_ecosim(self, biomass, params, n_super_individuals=200):
# Create initial population matching target biomass
...
def compute_step(self, prey_available, predation_pressure,
env_forcing, dt, spatial_context=None):
# Run one timestep: growth, mortality, reproduction, movement
biomass = ...
consumption = np.zeros(self.n_groups)
return IBMStepResult(
biomass=biomass,
production=0.0,
consumption_by_prey=consumption,
mortality_count=0.0,
recruitment_count=0.0,
)
def get_aggregate_biomass(self):
return sum(ind.weight * ind.n_represented / 1e6
for ind in self.individuals)
def get_consumption_by_prey(self):
return self._last_consumption.copy()
See pypath.ibm.smelt.SmeltIBM for a complete 530-line reference
implementation.
Inspecting IBM State¶
After running, inspect the super-individual population:
# Total biomass
print(f"IBM biomass: {smelt.get_aggregate_biomass():.4f} t/km2")
# Population size
print(f"Super-individuals: {len(smelt.individuals)}")
# Age distribution
ages = [ind.age for ind in smelt.individuals]
print(f"Age range: {min(ages):.1f} - {max(ages):.1f} years")
# Size distribution
weights = [ind.weight for ind in smelt.individuals]
print(f"Weight range: {min(weights):.1f} - {max(weights):.1f} g")
# Consumption breakdown
consumption = smelt.get_consumption_by_prey()
for i, c in enumerate(consumption):
if c > 0:
print(f" Consumed from group {i}: {c:.6f} t/km2")
Visualization in the Shiny App¶
The PyPath Shiny app provides a dedicated IBM page with:
- Biomass comparison: IBM vs standard Ecosim trajectories
- Size distribution: Histogram of individual body weights
- Age structure: Population age pyramid
- Energy reserves: Distribution of condition factor
- Spatial distribution: Patch-level biomass map (Ecospace mode)
Launch the app and navigate to Advanced > Individual-Based Model.