How to Wrap an External Simulator
You can wrap an SBML solver, NeuroML toolchain, or custom native engine as a BioModule and compose it inside the same communication-step world as other modules.
Wrapper pattern
import biosimulant as biosim
class TelluriumSBMLModel(biosim.BioModule):
execution_policy = biosim.ExecutionPolicy.EACH_WINDOW
def __init__(self, sbml_path: str):
import tellurium as te
self.runner = te.loadSBMLModel(sbml_path)
self.history = []
def inputs(self):
return {
"parameters": biosim.SignalSpec.record(
schema={"k1": "float64", "k2": "float64"},
)
}
def outputs(self):
return {
"state": biosim.SignalSpec.record(
schema={"species": "dict[str,float64]"},
)
}
def execute(self, inputs, *, context: biosim.ExecutionContext):
assert context.window_start is not None
assert context.window_end is not None
params = inputs.get("parameters")
if params is not None:
for name, value in params.value.items():
self.runner[name] = value
start = context.window_start
end = context.window_end
self.runner.simulate(start, end, steps=max(1, int((end - start) / 0.001)))
species = {sid: self.runner[sid] for sid in self.runner.getFloatingSpeciesIds()}
self.history.append([end, species])
return {"state": {"species": species}}
def snapshot(self):
return {"history": list(self.history)}
def restore(self, snapshot):
self.runner.reset()
self.history = [list(item) for item in snapshot.get("history", [])]Manifest
schema_version: "2.0"
title: "SBML Glycolysis Model"
description: "tellurium/libroadrunner wrapper"
standard: sbml
biosim:
entrypoint: "src.sbml_wrapper:TelluriumSBMLModel"
init_kwargs:
sbml_path: "data/repo/glycolysis.xml"
communication_step: 0.01
runtime:
python_version: "3.12"
dependencies:
packages:
- tellurium>=2.2
- numpy>=1.24Design guidance
- Initialize the external engine in
__init__, not lazily per window. - Advance the solver across
context.window_startandcontext.window_endinsideexecute()and explicitly selectEACH_WINDOW. - Return raw values that match the declared output specs; BioWorld validates, wraps, and timestamps them at the boundary.
- Implement snapshot/restore as honestly as the external engine allows.
- Use
ONCE_BEFORE_RUNfor one-shot external tools andONCE_AFTER_RUNfor report, export, or visualisation modules that consume final results.
Compatibility: Tellurium SBML convenience base
For Tellurium-backed SBML wrappers, biosimulant.contrib.sbml.TelluriumSBMLBioModule
provides an opt-in base that keeps Tellurium imports lazy and centralizes the
generic SBML scaffolding: XML patching for uninitialized parameters, observable
discovery, simulation windowing, summary outputs, headline averaging, and scalar
override handling. It retains the temporal compatibility lifecycle for existing
wrappers and solvers that require its fixed-step or settle behavior.
from biosimulant.contrib.sbml import TelluriumSBMLBioModule
class KineticModel(TelluriumSBMLBioModule):
_SBML_ID = "BIOMD..."
_TITLE = "Kinetic Model"
_OBSERVABLE_STRATEGY = "species"
_PARAMETER_INPUTS = {
"stimulus": ("k1", 1.0, "1/s", "Stimulus rate constant."),
}
_HEADLINE_OUTPUTS = {
"product": ("S2", "substance", "Product amount."),
}
def __init__(self, model_path: str = "data/model.xml", integration_step: float = 1.0):
super().__init__(model_path=model_path, integration_step=integration_step)Keep domain-specific payloads in the wrapper. If a sibling visualisation model
needs private data, override visualisation_aux_schema(),
visualisation_extra_selections(), and visualisation_aux_payload() locally
rather than adding domain assumptions to the generic SBML base.
Some external solvers are not trivially snapshot-safe. If the engine cannot restore internal state directly, document that limitation and rebuild or replay state explicitly in restore().