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

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

```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/add-onnx-model)
- [Create a BioModule](/how-to/create-biomodule)
