fix: event-time sensitivity guard misses state-dependent triggers reached through SBML (#52)#60
Merged
Conversation
…ched through SBML (#52) The guard refuses forward sensitivities when an event's crossing time depends on a requested sensitivity parameter, and it decides that from the trigger's bound addresses. It compared them against species concentrations only. ModelBuilder registers a species as an ExprTk variable only when its name is still free (model_builder.cpp), and SBML models routinely give each species an observable of the same name — so the species registration is skipped and a trigger token binds to &obs.total, never to &sp.concentration. Every SBML state-dependent trigger therefore slipped the guard and was answered instead of refused, with the event contributions missing entirely. On AMICI's `neuron` fixture (Izhikevich, trigger `v > 30`, which names no parameter but whose crossing time depends on `a` and `b` through the trajectory) the returned sensitivities were 6x-135x off, uniformly in one direction, across all four parameters — while the two fixtures whose triggers name a parameter outright were correctly refused. The guard now tests against every address that carries live state: species concentrations, observable totals, and rateOf accessors. An observable total is a linear functional of the state and a rateOf accessor is dx/dt, so a trigger reading either has a non-zero dt*/dp exactly as a concentration read does. Refusing is unchanged as a policy — this is only the coverage of what counts as state — and the message now names which of the three it saw and why that implies a moving crossing time. Tests: an SBML model whose trigger reads a species (the `neuron` shape) must be refused, through both `run` and `compute_all_sensitivities`, with an explicit assertion of the species/observable name collision that caused the miss. Both fail on the unmodified core with "DID NOT RAISE". The existing ModelBuilder-path state-dependent test still passes, as do the no-false-positive cases: fixed-time events, a parameter-valued reset, and the discontinuity-trigger pulse model. Closes #52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #52.
The bug
The guard refuses forward sensitivities when an event's crossing time depends on a requested sensitivity parameter, and it decides that from the trigger's bound addresses. It compared them against species concentrations only.
ModelBuilderregisters a species as an ExprTk variable only when its name is still free:SBML models routinely give each species an observable of the same name, so that registration is skipped and a trigger token binds to
&obs.total, never to&sp.concentration. Every SBML state-dependent trigger therefore slipped the guard and was answered instead of refused, with the event contributions missing entirely.Confirmed directly on the reported shape — the
SBML_EVENTfixture with its trigger swapped toS < 5:On AMICI's
neuronfixture (Izhikevich, triggerv > 30, which names no parameter but whose crossing time depends onaandbthrough the trajectory) the issue measured sensitivities 6x–135x off, uniformly in one direction, across all four parameters — whilediracandnested_events, whose triggers name a parameter outright, were correctly refused.The fix
The guard now tests against every address that carries live state: species concentrations, observable totals, and rateOf accessors. An observable total is a linear functional of the state and a rateOf accessor is
dx/dt, so a trigger reading either has a non-zerodt*/dpexactly as a concentration read does.Refusing is unchanged as a policy — as the issue says, "the codebase already does it well" — this is only the coverage of what counts as state. The message now names which of the three it saw and why that implies a moving crossing time.
Tests
neuronshape) must be refused, through bothrunandcompute_all_sensitivities. Both fail on the unmodified core with "DID NOT RAISE".compute_all_sensitivitieson a fixed-time model, and the discontinuity-trigger pulse model (n_events == 0, must stay allowed).Full Python suite: 2575 passed, 89 skipped.
Note on the issue's item 3
The issue separately asks whether the jump terms are handled at a fixed event time —
neuronassignsv = -candu = d + u, contributingd/dcandd/ddindependent ofdt*/dp. That is orthogonal to this guard and already covered:test_parameter_valued_reset_matches_fdpins∂h/∂pagainst finite differences for a parameter-valued assignment, andtest_additive_bolus_is_continuouspins the∂h/∂x = 1case.