How ToLibraryCreate a BioModule

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.txt

Step 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-packages

Notes

  • Emit ScalarSignal, ArraySignal, RecordSignal, or EventSignal, not raw Python values.
  • Declare ports as dict[str, SignalSpec], not sets of strings.
  • For generated files, emit a structure_artifacts record with *_file paths that exist when get_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.

Next steps