How ToCreate a BioModule

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

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

Notes

  • 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 update get_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.

Next steps