How ToLibraryWrap an External Simulator

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

Design guidance

  • Initialize the external engine in __init__, not lazily per window.
  • Advance the solver across [start, end] inside advance_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_steps high enough for that downstream module to consume the final committed outputs. A direct edge usually needs 1.

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().

Next steps