Skip to content

Model.set_param does not rebuild a species initial condition that references the parameter (species_ic_param_refs recorded, ic_state_dirty never set) #79

Description

@wshlavacek

Model.set_param updates the parameter but leaves any species initial condition that references it at its network-generation value. get_state() returns the stale amount, and reset() does not rebuild it either.

The dependency is already recorded and the invalidation flag already exists — NetworkModel.species_ic_param_refs names the (species, parameter) pair, and NetworkModel.ic_state_dirty stays False across the set_param call. So the machinery to invalidate appears to be in place and simply does not fire.

This is the network-path counterpart of #44, which fixed the same class of defect (a set_param that does not re-derive seed-species amounts) on the network-free path.

Reproducer

Self-contained, two species:

cat > minimal.net <<'EOF'
begin parameters
    1 Stot  100
    2 k     1
end parameters
begin species
    1 A()  Stot
    2 B()  0
end species
begin reactions
    1 1 2 k #A->B
end reactions
begin groups
    1 Obs_A  1
end groups
EOF
import bngsim

m = bngsim.Model.from_net("minimal.net")

print(m._core.species_ic_param_refs)  # [(0, 0)]  species 0's IC references parameter 0
print(m._core.ic_state_dirty)         # False
print(m.get_state()[0])               # 100.0

m.set_param("Stot", 1e6)

print(m.get_param("Stot"))            # 1000000.0   <- parameter did move
print(m._core.ic_state_dirty)         # False       <- never marked dirty
print(m.get_state()[0])               # 100.0       <- expected 1000000.0

m.reset()
print(m.get_state()[0])               # 100.0       <- reset does not rebuild it either
expected actual
get_param("Stot") 1e6 1e6 ✓
get_state()[0] 1e6 100.0 ✗
get_state()[0] after reset() 1e6 100.0 ✗
ic_state_dirty after set_param True False

On a published model

Same behaviour on a real network — scaffolded_mapk_cascade_kocieniewski2012 (85 species), where Stot is parameter 5 and species 3's initial condition (Scaff(map2k,map3k,mapk) Stot), and species_ic_param_refs reads [(3, 5)]:

m = bngsim.Model.from_net("B02_Kocieniewski_2012.net")
m.get_state()[3]                 # 100000.0
m.set_param("Stot", 1e6)
m.get_state()[3]                 # 100000.0   <- expected 1e6

Why it matters

A dose scan over a total amount — the common case, since a "total" is exactly the kind of parameter a species initial condition is written in terms of — silently returns identical results at every dose, with no error and no warning. Nothing in the API surface indicates the scan did not take effect: get_param confirms the new value.

It is also a cross-engine trap. AMICI, reading the same system through SBML initialAssignment, moves correctly, so a comparison scanning such a parameter disagrees for a reason that has nothing to do with the solver under test. Found while building a cross-engine steady-state dose scan, where it affected 3 of 6 published models (Stot, Rtot, APCtot) and had to be worked around by rewriting the parameter in the .net and reloading.

Version

bngsim 0.11.35 (build v0.11.35-72-g201836f), macOS 15 / darwin 24.6.0, Python 3.12.

Suggested fix

Have set_param / set_params consult species_ic_param_refs and set ic_state_dirty when the written parameter appears in it, so the existing rebuild path runs. Worth deciding explicitly whether the IC rebuild happens eagerly on set_param or lazily at the next get_state()/reset()/solve; the lazy form matches what the dirty flag implies. reset() in particular should rebuild from current parameter values rather than restore a snapshot taken at load time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions