Summary
The bngsim SBML bridge (pybnf/bngsim_sbml_model.py) fully reloads the bngsim
model — libSBML parse → interpretation → analytical-Jacobian derivation → codegen —
inside execute(), which runs once per objective evaluation. None of these
per-model costs amortize across a fit; in particular the symbolic Jacobian (SymPy
sp.diff) is re-derived from scratch on every evaluation, even though it depends only
on the rate-law structure and is identical across parameter sets.
The .net/BNGL bridge already avoids this (loads the model once, applies parameter
values in place via set_param), so the fix is to bring the SBML path in line.
Where (v1.4.0, 50bc026)
pybnf/bngsim_sbml_model.py, execute() — both the TimeCourse (L397) and
ParamScan (L422) branches:
doc = self._build_sbml_doc(mut=mut)
engine_model = self._load_bngsim_model_from_text(_sbml_doc_to_text(doc)) # full load, every eval
result = self._run_simulation(engine_model, ...)
_load_bngsim_model_from_text (L171) calls bngsim.Model.from_sbml_string /
from_sbml — the full loader (parse + interpret + analytical Functional Jacobian +
auto-codegen) — with no caching. execute() is invoked once per parameter set during
fitting/sampling, so the entire pipeline reruns each evaluation.
Cost
The dominant re-paid cost is the analytical-Jacobian derivation (SymPy). Measured with
bngsim 0.9.36:
| model |
analytical-Jacobian derivation (re-paid per eval) |
| BIOMD0000000001 (12 species) |
~27 ms |
| BIOMD0000000051 |
~785 ms |
It scales with model size. Parse + interpretation add a further ~10–40 ms re-paid per
eval, and the first evaluation in each worker process additionally pays the one-time
SymPy import (~0.3 s). Because the symbolic Jacobian is independent of parameter
values, every one of these re-derivations after the first is redundant work. As an
illustration, a 100k-evaluation fit of a BIOMD51-class model spends on the order of
~22 h re-deriving the identical Jacobian.
The .net bridge already does the right thing
pybnf/bngsim_model/net_model.py, execute():
model = self._engine_model # loaded once (Model.from_net), cached on the instance
model.set_param(pname, value) # apply new parameter values in place
model.reset()
Parameter values don't change the symbolic Jacobian, so set_param keeps the derived
Jacobian valid and the load cost is paid once for the whole fit.
Suggested fix
In the SBML path, load the bngsim model once (cache it on the instance, as the
.net path does) and apply per-evaluation parameter values via
model.set_param(...) + model.reset() instead of rebuilding the SBML document and
reloading. Reserve a reload for cases that genuinely change model structure (e.g.
mutants), not parameter-value changes.
Secondary: integration runs cold on every evaluation (both bridges)
Both bridges construct a fresh bngsim.Simulator(model, ...) per execute()
(bngsim_sbml_model.py:335; bngsim_model/net_model.py:267), so each integration
pays the one-time CVODE workspace / linear-solver setup (cold) instead of reusing a
warm solver. Measured on the same models, the cold→warm gap is ~2.5× on a fast model
(0.49 → 0.20 ms) and negligible on a slow/stiff one. Reusing the Simulator across
evaluations (with reset() to restore initial conditions) would let integration run
warm — a smaller win than the reload fix above, but the same idea of amortizing
per-model setup across a fit.
Note
This is a bridge issue, not a bngsim engine issue: bngsim reuses its solver workspace
correctly when a Simulator is reused, and supports set_param for in-place
parameter updates.
Summary
The bngsim SBML bridge (
pybnf/bngsim_sbml_model.py) fully reloads the bngsimmodel — libSBML parse → interpretation → analytical-Jacobian derivation → codegen —
inside
execute(), which runs once per objective evaluation. None of theseper-model costs amortize across a fit; in particular the symbolic Jacobian (SymPy
sp.diff) is re-derived from scratch on every evaluation, even though it depends onlyon the rate-law structure and is identical across parameter sets.
The
.net/BNGL bridge already avoids this (loads the model once, applies parametervalues in place via
set_param), so the fix is to bring the SBML path in line.Where (v1.4.0,
50bc026)pybnf/bngsim_sbml_model.py,execute()— both theTimeCourse(L397) andParamScan(L422) branches:_load_bngsim_model_from_text(L171) callsbngsim.Model.from_sbml_string/from_sbml— the full loader (parse + interpret + analytical Functional Jacobian +auto-codegen) — with no caching.
execute()is invoked once per parameter set duringfitting/sampling, so the entire pipeline reruns each evaluation.
Cost
The dominant re-paid cost is the analytical-Jacobian derivation (SymPy). Measured with
bngsim 0.9.36:
It scales with model size. Parse + interpretation add a further ~10–40 ms re-paid per
eval, and the first evaluation in each worker process additionally pays the one-time
SymPy import (~0.3 s). Because the symbolic Jacobian is independent of parameter
values, every one of these re-derivations after the first is redundant work. As an
illustration, a 100k-evaluation fit of a BIOMD51-class model spends on the order of
~22 h re-deriving the identical Jacobian.
The
.netbridge already does the right thingpybnf/bngsim_model/net_model.py,execute():Parameter values don't change the symbolic Jacobian, so
set_paramkeeps the derivedJacobian valid and the load cost is paid once for the whole fit.
Suggested fix
In the SBML path, load the bngsim model once (cache it on the instance, as the
.netpath does) and apply per-evaluation parameter values viamodel.set_param(...)+model.reset()instead of rebuilding the SBML document andreloading. Reserve a reload for cases that genuinely change model structure (e.g.
mutants), not parameter-value changes.
Secondary: integration runs cold on every evaluation (both bridges)
Both bridges construct a fresh
bngsim.Simulator(model, ...)perexecute()(
bngsim_sbml_model.py:335;bngsim_model/net_model.py:267), so each integrationpays the one-time CVODE workspace / linear-solver setup (cold) instead of reusing a
warm solver. Measured on the same models, the cold→warm gap is ~2.5× on a fast model
(0.49 → 0.20 ms) and negligible on a slow/stiff one. Reusing the
Simulatoracrossevaluations (with
reset()to restore initial conditions) would let integration runwarm — a smaller win than the reload fix above, but the same idea of amortizing
per-model setup across a fit.
Note
This is a bridge issue, not a bngsim engine issue: bngsim reuses its solver workspace
correctly when a
Simulatoris reused, and supportsset_paramfor in-placeparameter updates.