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):
def __init__(self, sbml_path: str):
import tellurium as te
self.runner = te.loadSBMLModel(sbml_path)
self.history = []
self._outputs = {}
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 set_inputs(self, signals):
params = signals.get("parameters")
if params is not None:
for name, value in params.value.items():
self.runner[name] = value
def advance_window(self, start: float, end: float) -> None:
result = 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])
self._outputs = {
"state": biosim.RecordSignal(
source="sbml",
name="state",
value={"species": species},
emitted_at=end,
spec=self.outputs()["state"],
)
}
def get_outputs(self):
return dict(self._outputs)
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
[start, end]insideadvance_window(). - Emit typed signals with
emitted_at=end. - Implement snapshot/restore as honestly as the external engine allows.
- If a one-shot external tool feeds a separate report/export/visualisation
module, set the lab’s
runtime.settle_stepshigh enough for that downstream module to consume the final committed outputs. A direct edge usually needs1.
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.
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().