<!-- Source: https://docs.biosimulant.com/how-to/library/wrap-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

```python

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

```yaml
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.

```python
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.

**Warning:**

  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

- [Add an ONNX Model](/how-to/library/add-onnx-model)
- [Create a BioModule](/how-to/library/create-biomodule)
