How ToWrap 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):
    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.24

Design guidance

  • Initialize the external engine in __init__, not lazily per window.
  • Advance the solver across context.window_start and context.window_end inside execute() and explicitly select EACH_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_RUN for one-shot external tools and ONCE_AFTER_RUN for 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().

Next steps