How to Create a BioModule
This guide builds a small module against the current communication-step kernel and shows the pieces you need for packaging and replay-safe execution.
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):
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 = []
self._latest_deaths = None
self._outputs = {}
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 set_inputs(self, signals):
self._latest_deaths = signals.get("deaths")
def advance_window(self, start: float, end: float) -> None:
if self._latest_deaths is not None:
self.count -= int(self._latest_deaths.value)
growth = int(self.count * self.growth_rate)
self.count += growth
self.history.append([end, self.count])
self._outputs = {
"population_state": biosim.RecordSignal(
source="population",
name="population_state",
value={"count": self.count},
emitted_at=end,
spec=self.outputs()["population_state"],
)
}
def get_outputs(self):
return dict(self._outputs)
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())Optional: use a convenience base
The raw BioModule pattern above is still the full-control contract. For
fixed-step models, StatefulBioModule can remove the repeated input, history,
and output publishing boilerplate.
class PopulationCounter(biosim.StatefulBioModule):
def __init__(self, initial_count: int = 100, growth_rate: float = 0.1):
super().__init__(integration_step=1.0, record_initial_state=True)
self.initial_count = int(initial_count)
self.growth_rate = float(growth_rate)
self.count = int(initial_count)
def outputs(self):
return {
"population_state": biosim.SignalSpec.record(
schema={"count": "int64"},
description="Current population count",
)
}
def reset_state(self):
self.count = self.initial_count
def step(self, h):
self.count += int(self.count * self.growth_rate)
def record_state(self, t):
self._history.append({"t": t, "count": self.count})
def output_payload(self, t):
return {"population_state": {"count": self.count}}Use raw BioModule when the module has unusual solver timing, external engine
semantics, or needs to construct signals itself.
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
- Emit
ScalarSignal,ArraySignal,RecordSignal, orEventSignal, not raw Python values. - Declare ports as
dict[str, SignalSpec], not sets of strings. - For generated files, emit a
structure_artifactsrecord with*_filepaths that exist whenget_outputs()returns. - 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 signals.get("port") as optional.