RuntimeBioModule API

BioModule API

BioModule is the single runnable unit in the communication-step kernel. Temporal modules advance state across windows; finite modules implement input-to-output computation behind the same contract.

Invocation policies require biosimulant>=0.0.26. From biosimulant>=0.0.33, a model’s model.yaml can repeat the policy as biosim.execution_policy; see Keep model.yaml in sync. Lab manifests don’t declare policies.

Contract

class BioModule:
    execution_policy = ExecutionPolicy.EACH_WINDOW
 
    def setup(self, config: dict[str, Any] | None = None) -> None: ...
    def reset(self) -> None: ...
    def set_inputs(self, signals: dict[str, BioSignal]) -> None: ...
    def execute(
        self,
        inputs: Mapping[str, BioSignal],
        *,
        context: ExecutionContext,
    ) -> Mapping[str, Any | BioSignal]: ...
    def advance_window(self, start: float, end: float) -> None: ...
    def get_outputs(self) -> dict[str, BioSignal]: ...
    def inputs(self) -> Mapping[str, SignalSpec]: ...
    def outputs(self) -> Mapping[str, SignalSpec]: ...
    def snapshot(self) -> dict[str, Any]: ...
    def restore(self, snapshot: Mapping[str, Any]) -> None: ...
    def visualize(self) -> VisualSpec | list[VisualSpec] | None: ...

New modules override the canonical execute() hook and explicitly declare a policy. Existing temporal modules may continue overriding advance_window() and normally get_outputs(). A module must not override both computation hooks.

Required semantics

  • execute(inputs, *, context) is the canonical hook for new finite and temporal modules.
  • advance_window(start, end) is the supported temporal compatibility hook and is not deprecated.
  • ExecutionPolicy.EACH_WINDOW is the compatibility default.
  • Use ONCE_BEFORE_RUN for preprocessing, finite inference, and pure one-shot pipelines; use ONCE_AFTER_RUN for final analysis and export.
  • Use canonical EACH_WINDOW when AI computation consumes evolving state or a new settle-independent temporal module advances through positive windows.
  • A canonical module that implicitly inherits EACH_WINDOW is accepted with a registration warning; first-party and generated modules declare it explicitly.
  • Canonical get_outputs() returns only the latest result successfully normalized and committed by BioWorld. Direct execute() calls do not update this cache.
  • Existing temporal get_outputs() must return declared typed signals.
  • inputs() and outputs() must return port -> SignalSpec mappings when the module participates in validated wiring.
  • Signals should be emitted as ScalarSignal, ArraySignal, RecordSignal, or EventSignal.
  • snapshot() / restore() should round-trip the module state needed for deterministic continuation from a communication boundary.

Canonical temporal example

import biosimulant as biosim
 
 
class Gain(biosim.BioModule):
    execution_policy = biosim.ExecutionPolicy.EACH_WINDOW
 
    def __init__(self, gain: float = 1.0) -> None:
        self.gain = float(gain)
 
    def inputs(self):
        return {
            "x": biosim.SignalSpec.scalar(
                dtype="float64",
                max_age=0.2,
                stale_policy="warn",
            )
        }
 
    def outputs(self):
        return {"y": biosim.SignalSpec.scalar(dtype="float64")}
 
    def execute(self, inputs, *, context):
        latest = inputs.get("x")
        if latest is None:
            return {}
        assert context.window_start is not None
        assert context.window_end is not None
        return {"y": float(latest.value) * self.gain}
 
    def snapshot(self):
        return {"gain": self.gain}
 
    def restore(self, snapshot):
        self.gain = float(snapshot["gain"])

BioWorld stamps the result at context.window_end. Canonical modules never run during zero-time settle() in 0.0.26; retain the temporal compatibility hook when settling is scientifically required.

Finite-computation example

class Predictor(biosim.BioModule):
    execution_policy = biosim.ExecutionPolicy.ONCE_BEFORE_RUN
 
    def inputs(self):
        return {"features": biosim.SignalSpec.array(dtype="float32", shape=(4,))}
 
    def outputs(self):
        return {"score": biosim.SignalSpec.scalar(dtype="float64")}
 
    def execute(self, inputs, *, context):
        return {"score": float(predict(inputs["features"].value))}

BioWorld wraps raw results using the output specs and stamps them at the current BioWorld boundary. Connected non-optional inputs must be available before invocation. Completed once-policy modules are not polled or retimestamped.

Valid phase edges move from before-run to each-window to after-run. Cycles remain valid only inside the each-window phase.

Keep model.yaml in sync

When you package a module, repeat its policy in model.yaml so Studio, Desktop and the Hub can tell when it runs without importing your code:

biosim:
  entrypoint: "src.predictor:Predictor"
  communication_step: 0.01
  execution_policy: once_before_run

The class attribute still decides when BioWorld runs the module. The manifest value is checked against it, so change both together:

  • The runtime refuses to load a model whose declared policy differs from the constructed module’s policy. The error names both values.
  • biosimulant labs validate reads the source without importing it. It fails when the source clearly disagrees with the declaration and warns when it can’t verify it.

Declare the value your module resolves to:

  • a class attribute resolves to that value;
  • self.execution_policy set to a fixed value in __init__ resolves to that value;
  • an execute() module that sets nothing resolves to each_window. BioWorld warns about the implicit default unless model.yaml declares each_window;
  • an advance_window() module always resolves to each_window.

Leave execution_policy out of model.yaml when __init__ chooses the policy from a constructor parameter. A lab can’t override a module’s policy. See Execution policy for the field reference.

ExecutionContext

ExecutionContext is immutable. run_start and run_end describe the positive public BioWorld.run() call. window_start and window_end are present only for EACH_WINDOW; simulated_time resolves to the start, window end, or run end for before, window, or after invocation respectively. Wall-clock IDs, retries, and platform scheduling metadata are deliberately excluded.

BioWorld.run(duration <= 0) remains setup-only, so once policies require a positive-duration run. ONCE_AFTER_RUN runs only after all positive windows commit successfully.

Convenience subclasses

BioModule remains the escape hatch for full control. Two optional subclasses remove common adapter boilerplate without changing the base contract.

SignalEmitterBioModule

Use SignalEmitterBioModule when your module owns its lifecycle but wants shared output storage and typed signal wrapping.

class Reporter(biosim.SignalEmitterBioModule):
    def outputs(self):
        return {"summary": biosim.SignalSpec.record(schema={"count": "int"})}
 
    def advance_window(self, start, end):
        self.publish_outputs(end, {"summary": {"count": 3}})

It provides source_name(), emit_signal(), publish_outputs(), get_outputs(), and clear_outputs().

StatefulBioModule

Use StatefulBioModule for fixed-step stateful models. Subclasses implement biology hooks and let the base handle window stepping, input overrides, and bounded history.

class Counter(biosim.StatefulBioModule):
    def __init__(self):
        super().__init__(integration_step=0.1, record_initial_state=True)
        self.count = 0
 
    def outputs(self):
        return {"count": biosim.SignalSpec.scalar(dtype="int64")}
 
    def step(self, h):
        self.count += 1
 
    def record_state(self, t):
        self._history.append({"t": t, "count": self.count})
 
    def output_payload(self, t):
        return {"count": self.count}

Port declarations

def inputs(self):
    return {
        "state": biosim.SignalSpec.record(
            schema={"v": "float64", "u": "float64"},
            max_age=0.001,
            stale_policy="error",
        )
    }
 
def outputs(self):
    return {
        "spikes": biosim.SignalSpec.event(
            schema={"ids": "list[int]"},
            description="Discrete spike deliveries",
        )
    }

Outputs declare the emitted profile. Inputs declare the accepted profile and freshness policy.

Durability

reset() is an optional convenience hook for UI-driven workflows you own. The kernel durability contract is snapshot() / restore(), which is what BioWorld.branch() and snapshot-based reruns rely on.

Notes

  • The world binds emitted outputs to the registered module name and the declared output port.
  • Empty output mappings do not clear prior committed state outputs from that module.
  • visualize() is optional and should return JSON-serializable visual specs.

See also