Uh oh!
There was an error while loading. Please reload this page.
feat(sensitivity): carry the equilibration's dx/dθ into a parameter scan (#81) - #110
Merged
Merged
Conversation
…can (#81) A dose-response experiment that pre-equilibrates and then scans could not be fit by a gradient method: parameter_scan / bifurcate refused every sensitivity-configured Simulator. The refusal was right — each point starts from the equilibrated snapshot, so re-seeding it as if it were the model's seed ICs discards the dx/dθ accumulated during the equilibration — but there was no correct option to offer instead. carry_sensitivities=True already had the right semantics for a sequential two-phase run; what was missing is that every primitive which *restores* a state dropped the derivative. Each is fixed where the state lives: * save_concentrations() redefines the IC baseline to the current state, whose dx/dθ did not change — so the new baseline inherits it and reset() restores both. A baseline saved with nothing carried is θ-independent literal ICs, i.e. the pre-#81 fresh start, so reset() on an ordinary model is unchanged. * save_concentrations(label=…) / restore_concentrations(label) capture and restore a named snapshot's dx/dθ the same way. * parameter_scan / bifurcate restore the reset target's state AND its dx/dθ per point, run each point with carry_sensitivities=True, and leave the model exactly as found. A continuation scan chains each point's dx/dθ from the previous point instead. NetworkModel gained the write half of the seed accessor (set_pending_sensitivity_seed) — the primitive that lets a protocol restore a state together with its θ-derivative — plus has_baseline_sensitivity_seed. Measured against a closed form (preequil_prod_deg.net, four doses off one equilibration): carried max rel err 1.2e-10 … 1.2e-9, versus a per-point fresh re-seed that is 100% wrong at t=0 and still 9.5%/15.3% at t=3 on the lowest dose (identical trajectories — the seed is the only difference). The scored steady-state variant matches dA_ss/dθ to 1e-12. On IGF1R_model_v1 (589 species / 4198 reactions, 1107 of 1178 carried seed entries live) d(pY980)/d{kp,kdp} matches central FD over the full protocol at 1e-8 … 1e-9, stable across three step sizes, with the whole three-dose scan running off one equilibration. Also closes a silent version of the same defect that needed no scan: equilibrate → save_concentrations() → run(sensitivities) used to drop the carry *and* clear the carried-over flag, so it re-seeded a θ-dependent IC as a fresh start and returned wrong derivatives with no warning — which is exactly the BNG simulate(steady_state) → saveConcentrations() → parameter_scan ordering. Refusals (never a silently re-seeded gradient): no matching carried dx/dθ, a scanned parameter that is itself differentiated, sensitivity_ic across the boundary, or an on_point hook that moves a differentiated parameter. An on_point literal setConcentration dose zeroes that species' seed row; a hook needing a θ-dependent override installs the rows itself and they are used verbatim.
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#81.
The gap
A pre-equilibrate → scored dose scan could not be gradient-fit:
parameter_scan/bifurcaterefused every sensitivity-configuredSimulator. As the issue says, therefusal is right — each point's initial condition is the equilibrated snapshot
x_ss(θ),so re-seeding it as if it were the model's seed ICs discards the
dx/dθaccumulatedduring the equilibration. The gap was that there was no correct option instead.
Answering the issue's two questions:
carry_sensitivitiesalready give the right semantics across the boundary?Yes — for a sequential two-phase run. What was missing is that every primitive which
restores a state dropped the derivative on the way:
set_state()/restore_concentrations()clear the pending seed, andsave_concentrations()cleared itand the "carried-over" flag.
xanddx/dp? Not a clone —it needs the restore path to carry the derivative with the state. Then the existing
per-point loop just passes
carry_sensitivities=True.What changed
Model.save_concentrations()redefines the IC baseline to the current state; the statedid not change, so neither did its
dx/dθ— the new baseline inherits it, andreset()restores both. A baseline saved with nothing carried is θ-independent literalICs, i.e. the pre-No way to carry forward sensitivities through a pre-equilibration into a scored parameter scan (blocks gradient fitting of pre-equilibrated dose-response) #81 fresh start, so
reset()on an ordinary model (and theevery-action-reset backends) is unchanged.
save_concentrations(label=…)/restore_concentrations(label)capture and restore anamed snapshot's
dx/dθthe same way.Simulator.parameter_scan/bifurcaterestore the reset target's state and itsdx/dθper point, integrate each point withcarry_sensitivities=True, and leave themodel — state, scanned parameter, carried derivative, dirty flag — exactly as found. A
continuation scan (
reset_conc=False) chains each point'sdx/dθfrom the previouspoint, so the whole sweep is one differentiable protocol.
NetworkModelgained the write half of the seed accessor,set_pending_sensitivity_seed(seed, param_names)— the primitive that lets any protocolrestore a state together with its θ-derivative (PyBNF drives its own per-point loop, so
it can use this directly without
parameter_scan) — plushas_baseline_sensitivity_seedand a writableic_state_dirtyfor the restore.Measurements
Oracle is a closed form, not a finite difference:
preequil_prod_deg.netisdA/dt = k_prod − (k_deg + dose)·Aequilibrated atdose = 0, so every point'sdA/dθis exact. Four doses off one equilibration:t=0t=3(k_prod/k_deg)The trajectories are identical to 1e-7 in every row — the seed is the only difference. The
same scan scored at each point's own steady state matches
dA_ss/dθ = (1/λ, −k_prod/λ²)to 1e-12. Note the shape of the wrong column: re-seeding is 100% off at
t=0and itserror decays with dose, so it is worst at the low-dose end of a dose-response — the
informative part of the fit — and looks fine at saturating dose.
On the issue's own model family (
IGF1R_model_v1, 589 species / 4198 reactions,equilibrated at a basal ligand dose so 1107 of 1178 carried seed entries are live, then
scanned over three doses applying each with
on_point+set_concentration):d(pY980)/d{kp, kdp}matches central FD of the measured observable over the fullprotocol at 1e-8 … 1e-9, stable across
h/p = 1e-3, 1e-4, 1e-5(signal, not FDnoise), and the whole scan runs off ONE equilibration in 2.0 s. The scan is also
bit-identical to the hand-rolled equivalent (restore snapshot → install seed →
run(carry_sensitivities=True)) on every point.A silent defect this also closes
No scan required:
equilibrate → save_concentrations() → run(sensitivities)used to dropthe carry and clear the carried-over flag, so it re-seeded a θ-dependent initial
condition as a fresh start and returned wrong derivatives with no warning (100% off at
t=0on the model above;reset()after the save had the same hole). That is exactly theBNG action ordering
simulate({steady_state=>1}) → saveConcentrations() → parameter_scan, so a fix that only worked from the live state would not have unblockedthe protocol shape in the issue.
Refusals kept (never a silently re-seeded gradient)
dx/dθSimulatorwith these params, or a plain run /set_concentration()/set_state()dropped it)sensitivity_paramsentrysensitivity_icacross the boundary∂y/∂y_k(0)has no meaningon_pointhook moves a differentiated parameterAn
on_pointhook may apply the usual coupledsetConcentrationdose override:assigning species k a literal value makes
∂x_k(0)/∂θ = 0for that species (what aliteral assignment means, and what
set_concentrationalready documents) while the restkeep the carried derivative. The one case bngsim cannot infer is an override computed
from a differentiated parameter — such a hook installs the correct rows itself with
set_pending_sensitivity_seedafter its concentration writes, and a seed still pendingwhen the hook returns is used verbatim.
Tests / verification
python/tests/test_preequilibration_sensitivity.py— newTestScanCarryOver(closed-formper point, per-point
t=0seed, observable output sensitivities, scored steady-statedose-response, the BNG
save_concentrationsordering,reset_tonamed snapshot,continuation == a chain of sequential carry runs,
on_pointliteral-dose zero row and theinstall-your-own-seed escape hatch, "left as we found it", and the five refusals), plus
baseline-inheritance and seed-write round-trip/validation cases.
python/tests/test_parameter_scan.py— the old blanket-refusal test split into the tworefusals that remain.
python/tests/test_model_clone.py— theImplclone contract for the pending andbaseline seeds (new fields ⇒ new cases, per the contract comment).
python/tests, in-tree); C++ctest5/5;pre-commit(ruff, ruff-format, clang-format, mypy) clean.sensitivity workflow the only behavior change is that a plain scan now rewinds the
carried-over flag it used to leave set.