How to Create a BioModule
This guide builds a temporal module against the communication-step kernel and
shows how finite computations use the same BioModule contract.
Project structure
my-model/
src/
my_module.py
model.yaml
requirements.txtStep 1: implement the module
# src/my_module.py
import biosimulant as biosim
class PopulationCounter(biosim.BioModule):
execution_policy = biosim.ExecutionPolicy.EACH_WINDOW
def __init__(self, initial_count: int = 100, growth_rate: float = 0.1):
self.initial_count = int(initial_count)
self.count = int(initial_count)
self.growth_rate = float(growth_rate)
self.history = []
def inputs(self):
return {
"deaths": biosim.SignalSpec.scalar(
dtype="int64",
max_age=1.0,
stale_policy="warn",
)
}
def outputs(self):
return {
"population_state": biosim.SignalSpec.record(
schema={"count": "int64"},
description="Current population count",
)
}
def execute(self, inputs, *, context):
deaths = inputs.get("deaths")
if deaths is not None:
self.count -= int(deaths.value)
growth = int(self.count * self.growth_rate)
self.count += growth
end = context.window_end
assert end is not None
self.history.append([end, self.count])
return {"population_state": {"count": self.count}}
def snapshot(self):
return {
"count": self.count,
"history": list(self.history),
}
def restore(self, snapshot):
self.count = int(snapshot["count"])
self.history = [list(point) for point in snapshot.get("history", [])]
def visualize(self):
return {
"render": "timeseries",
"data": {
"title": "Population Over Time",
"xlabel": "Time",
"ylabel": "Count",
"series": [{"name": "Population", "points": self.history}],
},
}Step 2: smoke-test it locally
import biosimulant as biosim
from src.my_module import PopulationCounter
world = biosim.BioWorld(communication_step=1.0)
builder = biosim.WiringBuilder(world)
builder.add("population", PopulationCounter(initial_count=50, growth_rate=0.05))
builder.apply()
world.run(duration=10.0)
print(world.collect_visuals())Choose a policy
The example above uses EACH_WINDOW because its state changes every window. Use
ONCE_BEFORE_RUN for one-shot preprocessing or inference, and ONCE_AFTER_RUN
for final analysis or export. Existing fixed-step models that need zero-time
settling can keep advance_window() or StatefulBioModule. The
BioModule API covers both. No manifest
execution field is required.
Step 3: write the manifest
schema_version: "2.0"
title: "Population Counter"
description: "Simple growth model with an optional deaths input"
standard: other
biosim:
entrypoint: "src.my_module:PopulationCounter"
init_kwargs:
initial_count: 100
growth_rate: 0.1
communication_step: 1.0
io:
inputs:
- name: deaths
signal_type: scalar
dtype: int64
outputs:
- name: population_state
signal_type: record
schema:
count: int64
runtime:
python_version: "3.12"
dependencies:
packages: []Step 4: build the package
Add this model to biosimulant-packages.yaml, then build from the repository root:
biosimulant labs release build biosimulant-packages.yaml --out dist/biosimulant-packagesNotes
- Temporal hooks emit typed signals. Execute-style hooks may return raw values;
BioWorld validates and wraps them using declared output specs, then replaces any
caller-supplied timestamp with
context.simulated_time. - Direct
execute()calls return the author’s raw mapping and do not updateget_outputs(); only a successful BioWorld commit updates that cache. - Canonical modules do not run during zero-time
settle(). - Declare ports as
dict[str, SignalSpec], not sets of strings. - Use
snapshot()/restore()for replay-safe state. Do not rely on a kernel-level reset API. - Use
reset()only if you own a higher-level workflow that calls it explicitly.
⚠️
A consumer may not receive a signal on every boundary. Always treat inputs.get("port") as optional.