Skip to content

SBML/Antimony fits ignore the experiment's measurement grid (SBML sim path drops sample_times; non-integer data times fail) #470

Description

@wshlavacek

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/).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions