Summary
When fitting an SBML or Antimony model on the new-era experiment: / data:
surface, PyBNF's SBML simulation path ignores the experiment's measurement times
and instead simulates on a uniform grid (t_span=(0, t_end), n_points). As a
result, the fit only works when the data's independent-variable values happen to
land on that uniform grid (e.g. integer times up to t_end); a data point at a
non-grid time (e.g. t = 0.5) is never in the simulation output, and scoring
fails.
This is a PyBNF bug, not a bngsim bug — bngsim already supports arbitrary output
times, and the native BNGL path already uses them; only the SBML/Antimony wrapper
drops them.
Symptom
- Offline / direct scoring: raises at
pybnf/objective.py:324
Experimental data includes time=0.5, but that time is not in the simulation output.
- Real CLI: every evaluation scores
inf (the failed match is swallowed into an
infinite objective), so the fit "completes" at objective = inf and recovers
nothing.
The same model/data on bngl_backend = bngsim (native BNGL) fits and recovers
correctly at the identical non-integer times.
Root cause
- The
TimeCourse action already carries the experiment's measurement grid:
TimeCourse.__init__(self, d, explicit_points=None) — "Optional iterable of
independent-variable (time) values at which the simulation should output, derived
from an experiment's data (ADR-0028)" (pybnf/pset.py).
- bngsim already supports arbitrary output times:
bngsim.Simulator.run(..., sample_times: list[float] | None = None, ...) (bngsim
0.9.67).
- The native BNGL path uses it:
pybnf/bngsim_model/net_model.py:816-819
calls plan.sim.run(..., sample_times=plan.sample_times).
- The SBML/Antimony path does not:
pybnf/bngsim_sbml_model.py:879-884
handles a TimeCourse by calling
self._run_simulation(engine_model, act.time, act.stepnumber + 1, ...), and
_run_simulation runs
sim.run(t_span=(0.0, end_time), n_points=int(n_points)) — a uniform grid from
act.time/act.stepnumber. The action's explicit_points (the actual data
times) are never threaded in.
Reproduction
Model (decay.ant):
model decay
species A = 100, B = 0;
k = 0.5;
conv: A -> B; k*A;
end
Conf (fit.conf):
edition = 2
model: decay.ant
sbml_backend = bngsim
job_type = de
objective = sos
observable: Obs_A, formula: A
experiment: timecourse, data: decay.exp
uniform_var = k 0.05 3.0
population_size = 20
max_iterations = 40
decay.exp with integer times 0,1,…,8 (Obs_A = 100*exp(-0.5 t)) → recovers
k = 0.5. ✅
decay.exp with any non-integer time (e.g. add t = 0.5) → fails as above. ❌
The committed tutorial lesson examples/tutorial/11_interop/ is exactly this model
in BNGL/SBML/Antimony; it deliberately uses an integer grid to sidestep the bug, and
its README documents the limitation. It's a ready-made regression fixture: flip its
grid to non-integer and the two SBML/Antimony confs should still recover k once
this is fixed.
Suggested fix
In bngsim_sbml_model.py, thread the TimeCourse action's explicit_points (the
measurement grid) into the simulation, i.e. pass sample_times=... to
sim.run(...) in _run_simulation (mirroring net_model.py's BNGL path) instead
of a uniform n_points grid when the action carries explicit points. No bngsim
change is required. (The ParamScan path at bngsim_sbml_model.py:899 has the same
uniform-grid assumption over linspace(min, max, stepnumber+1) and is worth
checking in the same pass.)
Not a bngsim bug
bngsim.Simulator.run already accepts sample_times; the engine supports arbitrary
output times. The gap is entirely in PyBNF's SBML/Antimony wrapper not passing the
grid it already holds.
Context
- PyBNF
main @ d6a84ad; bngsim 0.9.67; petab 0.8.2.
- Surfaced while building the edition-2 feature tutorial (
examples/tutorial/11_interop/).
Summary
When fitting an SBML or Antimony model on the new-era
experiment:/data:surface, PyBNF's SBML simulation path ignores the experiment's measurement times
and instead simulates on a uniform grid (
t_span=(0, t_end),n_points). As aresult, the fit only works when the data's independent-variable values happen to
land on that uniform grid (e.g. integer times up to
t_end); a data point at anon-grid time (e.g.
t = 0.5) is never in the simulation output, and scoringfails.
This is a PyBNF bug, not a bngsim bug — bngsim already supports arbitrary output
times, and the native BNGL path already uses them; only the SBML/Antimony wrapper
drops them.
Symptom
pybnf/objective.py:324inf(the failed match is swallowed into aninfinite objective), so the fit "completes" at
objective = infand recoversnothing.
The same model/data on
bngl_backend = bngsim(native BNGL) fits and recoverscorrectly at the identical non-integer times.
Root cause
TimeCourseaction already carries the experiment's measurement grid:TimeCourse.__init__(self, d, explicit_points=None)— "Optional iterable ofindependent-variable (time) values at which the simulation should output, derived
from an experiment's data (ADR-0028)" (
pybnf/pset.py).bngsim.Simulator.run(..., sample_times: list[float] | None = None, ...)(bngsim0.9.67).
pybnf/bngsim_model/net_model.py:816-819calls
plan.sim.run(..., sample_times=plan.sample_times).pybnf/bngsim_sbml_model.py:879-884handles a
TimeCourseby callingself._run_simulation(engine_model, act.time, act.stepnumber + 1, ...), and_run_simulationrunssim.run(t_span=(0.0, end_time), n_points=int(n_points))— a uniform grid fromact.time/act.stepnumber. The action'sexplicit_points(the actual datatimes) are never threaded in.
Reproduction
Model (
decay.ant):Conf (
fit.conf):decay.expwith integer times0,1,…,8(Obs_A = 100*exp(-0.5 t)) → recoversk = 0.5. ✅decay.expwith any non-integer time (e.g. addt = 0.5) → fails as above. ❌The committed tutorial lesson
examples/tutorial/11_interop/is exactly this modelin BNGL/SBML/Antimony; it deliberately uses an integer grid to sidestep the bug, and
its README documents the limitation. It's a ready-made regression fixture: flip its
grid to non-integer and the two SBML/Antimony confs should still recover
koncethis is fixed.
Suggested fix
In
bngsim_sbml_model.py, thread theTimeCourseaction'sexplicit_points(themeasurement grid) into the simulation, i.e. pass
sample_times=...tosim.run(...)in_run_simulation(mirroringnet_model.py's BNGL path) insteadof a uniform
n_pointsgrid when the action carries explicit points. No bngsimchange is required. (The
ParamScanpath atbngsim_sbml_model.py:899has the sameuniform-grid assumption over
linspace(min, max, stepnumber+1)and is worthchecking in the same pass.)
Not a bngsim bug
bngsim.Simulator.runalready acceptssample_times; the engine supports arbitraryoutput times. The gap is entirely in PyBNF's SBML/Antimony wrapper not passing the
grid it already holds.
Context
main@d6a84ad; bngsim 0.9.67; petab 0.8.2.examples/tutorial/11_interop/).