Repository navigation
Description
Summary
A measurement-model observable: <id>, formula: <expr> (ADR-0036) that references an SBML assignment-rule variable passes config-time validation but then fails at runtime, mid-fit, because assignment-rule variables are in the formula-validation namespace yet are never simulation-output columns. The validation namespace and the set of symbols actually resolvable at materialize are out of sync: validation says yes, the run says no.
This is distinct from #463 (which is about .ant models routing through parse_bngl). This one affects any SBML model (.xml and .ant), fails late (during the fit) rather than at config build, and is the opposite timing/path. Surfaced while finishing #386 deliverable 6 / #462.
Mechanism
Configuration._model_expression_namespace(pybnf/config.py:1858) builds the allowed-symbol set for an SBML model frompybnf.petab._sbml.parse_model(...).namespace_symbols, which includes assignment-rule targets (e.g. the Becker model'sEpo_medium := Epo + dEpoe,Epo_cells := Epo_EpoRi + dEpoi)._load_measurement_models(pybnf/config.py:1764) validates eachobservableFormula's free symbols against that set — so a formula referencing an assignment-rule variable passes and aMeasurementModelis built.- At fit time,
MeasurementModel.materialize(pybnf/measurement/base.py:101) resolves each symbol to a simulation-output column, a PSet value, or a constant. bngsim's SBML simulation outputs species only (not assignment-rule variables), and the symbol is neither a free parameter nor a constant — so it raises:
PybnfError: Measurement model '<id>' references 'Epo_cells', which is neither a
simulation-output column (...) nor a fit/model parameter. The measurement model
cannot be evaluated.
Reproduction
With tests/sbml_files/becker_epor.xml (sbml_backend = bngsim):
edition = 2
job_type = trf
objective = chi_sq
sbml_backend = bngsim
model: becker_epor.xml
observable: Epo_cells, formula: Epo_cells # Epo_cells is an SBML assignment rule
experiment: tc, data: tc.exp
uniform_var = kon 1e-5 1e-2
uniform_var = koff 1e-3 1e-1
parse_sbml(...).namespace_symbolscontainsEpo_cells→ True.Configuration(...)builds successfully (the measurement modelEpo_cellsis created).- Running the fit raises the
materializeerror above (the bngsim SBML output columns aretime, EpoR, Epo, Epo_EpoR, Epo_EpoRi, dEpoi, dEpoe— noEpo_cells/Epo_medium).
The working alternative is to reconstruct the observable from species — formula: Epo_EpoRi + dEpoi — which is the intended observation-layer path and what tests/test_gradient_optimizer.py::test_trf_multistart_smoke_on_becker_epor_sbml_measurement_layer (#462) does. The bug is specifically that referencing the assignment rule directly is accepted and then fails late.
Why it matters
- Config validation is supposed to be the typo-guard ("an unknown symbol is an error, not a new free parameter") but here it green-lights a symbol the run can't resolve, so the error lands deep in a fit instead of at load.
- It's a natural thing to write: SBML authors define exactly these convenience variables (the D2D
Epo_cells/Epo_mediumobservables are assignment rules), so reaching for them by name is the obvious first attempt.
Suggested fix
Reconcile the validation namespace with what's resolvable at materialize. Either:
- Exclude un-resolvable assignment-rule variables from
namespace_symbols(or have_load_measurement_modelsvalidate against species ∪ parameters ∪ compartments ∪ free-params only), so referencing one fails at config build with the existing pointed message; or - Make assignment-rule variables resolvable — output them as columns (roadrunner already exposes them), or evaluate the rule in the measurement layer — so
formula: Epo_cellssimply works.
Option 1 is the smaller, safer fix (fail fast, clear message); option 2 is the more user-friendly one. Either way, add a test for an assignment-rule symbol in a formula.
References
pybnf/config.py:1858(_model_expression_namespace),pybnf/config.py:1764(_load_measurement_models),pybnf/measurement/base.py:101(MeasurementModel.materialize).- Sibling issue Measurement-model formulas silently unavailable on Antimony (.ant) models — namespace built from parse_bngl #463 (the
.ant→parse_bnglnamespace gap). Parent context: Gradient-based local optimizers (D2D-style: TRF/Levenberg-Marquardt + L-BFGS-B) in the async Algorithm loop #386 deliverable 6 / Gradient optimizers: finish #386 deliverable 6 — convergence-vs-baseline test + becker smoke test #462.
Activity
fix(measurement): exclude SBML assignment-rule variables from the for…
fix(measurement): route Antimony (.ant) models through the SBML formu…
wshlavacek commentedon Jun 30, 2026
Fixed in e462ace (option 1: the smaller, safer fail-fast).
_sbml.parse_modelnow records<assignmentRule>targets (with their<ci>referents) and excludes them from the formula namespace;_load_measurement_modelsrejects a reference at config build with a pointed reconstruct-from-species message instead of failing mid-fit. Tests cover the scanner-level exclusion and config-build rejection on a crafted SBML and on the realbecker_epor.xmlfixture.Option 2 (make
formula: Epo_cellsjust work by resolving assignment-rule observables) is tracked separately in #465.feat(measurement): resolve SBML assignment-rule observables by inlini…
fix(measurement): exclude SBML assignment-rule variables from the for…
fix(measurement): route Antimony (.ant) models through the SBML formu…
feat(measurement): resolve SBML assignment-rule observables by inlini…