Skip to content

Read SBML initial assignments instead of the placeholder attribute they supersede - #801

Merged
wshlavacek merged 1 commit into
mainfrom
fix/795-sbml-initial-assignments
Sep 22, 2026
Merged

wshlavacek merged 1 commit into
mainfrom
fix/795-sbml-initial-assignments

Conversation

@wshlavacek

Copy link
Copy Markdown
Collaborator

Fixes #795.

SBML lets a model give an entity its starting value in two places: an attribute on
the entity (value on a parameter, size on a compartment, initialAmount or
initialConcentration on a species) and a listOfInitialAssignments entry that
supersedes it. When both are present the attribute is a placeholder the model never
starts from.

PyBNF's import-time SBML scanner read only the attributes, so it reported numbers the
model does not start from and said nothing about it. Two places used those numbers.
The measurement layer binds them as constants, so an observable or noise formula
naming such a parameter was scored with the placeholder. The exporter converts them
into absolute numbers for a relative condition, so a wrong number was written into
conditions.tsv. Where the model writes no attribute at all, which is what antimony
emits for a derived parameter, the failure was different and not much better: the
symbol stayed in the namespace with no value behind it, so a formula naming it died
during materialize saying it was neither a simulation output nor a model parameter,
which is not true.

What changes

An entity whose initial value an assignment derives has no value the model file
settles, so the scanner reports none.

An assignment that is arithmetic over numbers alone is the value, and it replaces the
attribute. The test for that is a small evaluator that walks the MathML and refuses an
identifier node, so "evaluates to a number" and "reads no other entity" are one test
rather than two. This is the same rule petab applies to an SBML initial assignment and
the rule we argued for upstream in PEtab-dev/libpetab-python#517 for BioNetGen.

An assignment computed from other entities drops the entity from the value map and
from the formula namespace, and records its right hand side. A formula naming the
entity is then rewritten to the entities it is computed from, through the same inliner
that already handles an assignment rule. So an observable over Bertozzi's beta_N,
which the model computes as (R0_ * gamma_) / N_ behind a placeholder value of 0.0, is
scored with the value the model starts from, and it tracks the fit when R0_ is
estimated rather than sitting at a number the file implied.

Inlining is refused when the assignment reads something that moves during the
simulation, because the substituted expression is read at a measurement time while the
assignment fixed a value at the start. The refusal names the entity that moves.

A species is deliberately treated differently. An initial assignment pins the start
only, so a species is still a dynamical state and still a simulation-output column. It
keeps its place in the namespace and is never inlined, and only loses a stale declared
initial. Treating it like an assignment-rule target would have broken the Boehm fixture
on the first run, since its observables name two species set this way.

Two adjacent defects are fixed here as well, because they are the same sentence. An
assignment rule target that carries a leftover value attribute no longer reports that
number either. A prediction noise formula now inlines the same map a measurement
formula does, so the two layers no longer disagree about which symbols a model offers.
The refusal in mutation_target_value no longer blames BNGL alone: it names the
parameter, gives both languages' version of the cause, and says what to write instead.
That message was already reachable for an SBML model before this change.

How much this reaches

Nothing committed to this repository is affected. Of 20 SBML files here, three have an
initial assignment and all three are on a species, and the field that held their stale
values had no reader. Outside it the pattern is common: of 25 models in the public
PEtab benchmark collection, 16 use initial assignments, 31 of those are on a parameter
or a compartment, and 28 of those 31 were getting a stale value out of the scanner. All
31 are computed from other entities rather than from numbers, which is why the fix is
built around inlining rather than around evaluating.

Verification

The full default test tier passes with BNG2.pl available: 5475 passed, 26 skipped. The
failure set on the petab modules is identical to the pre-change baseline. Lint is clean
under the pinned ruff. The new tests cover the scanner (value superseded, value
dropped, expression recorded, namespace membership, the antimony shape, compartments,
chains, block order, each kind of moving entity refused, untranslatable math, the
species asymmetry including a Boehm regression, and the numeric evaluator), the inliner
(Bertozzi and Laske shapes, chains, each error message), the measurement layer (the
inlined column is the value the model starts from, and it moves with a fitted
dependency), and the exporter refusal.

ADR-0147 records the decision and why the two rejected alternatives, refusing with a
better message or excluding without inlining, would turn 28 measured silent wrong
numbers into 28 refusals where a correct answer was available.

…ey supersede

SBML lets a model set an entity's starting value in two places. A parameter
carries a value attribute, a compartment a size, and a species an initial amount
or concentration, and a listOfInitialAssignments entry may supersede any of
them. When it does, the attribute is a placeholder the model never starts from.

The import-time scanner read only the attributes, so it reported numbers the
model does not start from, and said nothing. Two consumers used them. The
measurement layer binds them as constants, so an observable or noise formula
naming such a parameter was scored with the placeholder. The exporter turns them
into absolute numbers for a relative condition, so conditions.tsv got a wrong
value. A model that writes no attribute at all, which is what antimony emits for
a derived parameter, failed differently and not much better: the symbol stayed in
the namespace with no value behind it, so a formula naming it died at materialize
time saying it was neither a simulation output nor a model parameter, which is
not true.

An entity an initial assignment derives now has no value the model file settles,
so the scanner reports none. An assignment that is arithmetic over numbers alone
is the value and replaces the attribute. An assignment computed from other
entities drops the entity from the value map and the formula namespace and
records its right hand side, and the measurement layer inlines that right hand
side, which is what it already does for an assignment rule. So an observable over
Bertozzi's beta_N, which the model computes as (R0_ * gamma_) / N_ behind a
value of 0.0, is scored with the value the model starts from, and it tracks the
fit when R0_ is estimated.

Inlining is refused when the assignment reads something that moves during the
simulation, because the substituted expression is read at a measurement time
while the assignment fixed a value at the start. A species is deliberately left
alone beyond losing a stale initial: an initial assignment pins the start only,
so the species is still a dynamical state and still an output column, and
excluding it would break the Boehm fixture, whose observables name two species
set this way.

Two adjacent defects are fixed in the same pass because they are the same
sentence. An assignment rule target that carries a leftover value attribute no
longer reports that number either. A prediction noise formula now inlines the
same map a measurement formula does, so the two layers no longer disagree about
which symbols a model offers.

The refusal in mutation_target_value no longer blames BNGL alone. It names the
parameter, gives both languages' version of the cause, and says what to write
instead. That message was already reachable for an SBML model before this change.

Measured on 25 models from the public PEtab benchmark collection, 16 use initial
assignments, 31 of them on a parameter or compartment, and 28 of those were
reporting a stale value. No model committed to this repository has one on a
parameter, so nothing here changes. ADR-0147 records the decision.
@wshlavacek
wshlavacek merged commit c64faaf into main Sep 22, 2026
7 checks passed
@wshlavacek
wshlavacek deleted the fix/795-sbml-initial-assignments branch September 22, 2026 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PEtab export reads SBML initial values from the wrong place when an initial assignment sets them

1 participant