You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Gradient fits (fit_type = trf / lbfgs) currently cannot target a parameter_scan (dose-response) objective: the scan Data is assembled without a sensitivity tensor, so gradient assembly raises GradientNotSupported → PybnfError (pybnf/gradient/assembly.py:266-270 → pybnf/algorithms/optimizers/gradient_base.py:460-464). For the default dose-response path, this is a wiring gap, not a math gap.
Key observation — the tensor is already computed, then discarded
The default synthesized dose-response experiment (pybnf/config.py:1367-1386) uses steady_state=1 with ss_method omitted → parity / integrate-to-steady-state (pybnf/bngsim_model/classification.py:56-57), which runs each dose point via Simulator.run(steady_state=True) on a sensitivity-configured simulator (pybnf/bngsim_model/net_model.py:864-866; _scan_parity_steady_state). bngsim fully supports forward output sensitivities on run(steady_state=True).
So the ∂obs(dose_i)/∂θ tensor is already computed at every dose point — and then thrown away at row assembly:
_scan_result_to_row / _assemble_scan_data (net_model.py:1587-1623) read only observables/expressions and build a bare Data with output_sensitivities = None.
Contrast the time-course path _result_to_data (net_model.py:1657-1673), which does attach the tensor.
Because PyBNF hand-rolls the dose-response as a per-point run() loop (not Simulator.parameter_scan()), bngsim's parameter_scan sensitivity refusal is never even reached here.
Proposed change
Stack the per-point sensitivity rows down the dose axis into the scan Data.output_sensitivities (mirroring the time-course _result_to_data), so the existing gradient assembly (assembly.py) can consume them and produce d(objective)/dθ for dose-response objectives. The swept dose is the data's independent variable (not a fitted θ), so ∂obs/∂θ per dose is well-posed.
Scope / caveats to resolve
Parity only, initially.ss_method=>newton (KINSOL algebraic solve) performs no forward-sensitivity integration — it would need implicit-function-theorem sensitivities dx*/dθ = −(∂f/∂x)⁻¹ ∂f/∂θ from the fixed-point Jacobian. Continuation scans (reset_conc=0) carry a θ-dependent seed and would need sensitivity seed-chaining across points. Recommend supporting parity / reset-to-seed dose-response first and keeping Newton/continuation gradient-free with a clear message.
Steady-state sensitivity convergence (the real risk). The CVODE steady-state early-stop triggers on the state residual ‖f(t,y)‖₂ / n, not the sensitivity residual. dx_ss/dθ can converge on a slower timescale than x_ss in stiff systems, so the recovered tensor at the truncation row may be under-converged and yield subtly wrong gradients. Validate the recovered tensor against finite differences, and/or also bound the sensitivity residual before truncating.
Add a test comparing the assembled dose-response objective gradient against a finite-difference gradient of the same objective, on a small parity/steady-state dose-response fit.
Summary
Gradient fits (
fit_type = trf/lbfgs) currently cannot target aparameter_scan(dose-response) objective: the scanDatais assembled without a sensitivity tensor, so gradient assembly raisesGradientNotSupported→PybnfError(pybnf/gradient/assembly.py:266-270→pybnf/algorithms/optimizers/gradient_base.py:460-464). For the default dose-response path, this is a wiring gap, not a math gap.Key observation — the tensor is already computed, then discarded
The default synthesized dose-response experiment (
pybnf/config.py:1367-1386) usessteady_state=1withss_methodomitted → parity / integrate-to-steady-state (pybnf/bngsim_model/classification.py:56-57), which runs each dose point viaSimulator.run(steady_state=True)on a sensitivity-configured simulator (pybnf/bngsim_model/net_model.py:864-866;_scan_parity_steady_state). bngsim fully supports forward output sensitivities onrun(steady_state=True).So the
∂obs(dose_i)/∂θtensor is already computed at every dose point — and then thrown away at row assembly:_scan_result_to_row/_assemble_scan_data(net_model.py:1587-1623) read only observables/expressions and build a bareDatawithoutput_sensitivities = None._data_with_headers(pybnf/bngsim_sbml_model.py:939).Contrast the time-course path
_result_to_data(net_model.py:1657-1673), which does attach the tensor.Because PyBNF hand-rolls the dose-response as a per-point
run()loop (notSimulator.parameter_scan()), bngsim'sparameter_scansensitivity refusal is never even reached here.Proposed change
Stack the per-point sensitivity rows down the dose axis into the scan
Data.output_sensitivities(mirroring the time-course_result_to_data), so the existing gradient assembly (assembly.py) can consume them and produced(objective)/dθfor dose-response objectives. The swept dose is the data's independent variable (not a fitted θ), so∂obs/∂θper dose is well-posed.Scope / caveats to resolve
ss_method=>newton(KINSOL algebraic solve) performs no forward-sensitivity integration — it would need implicit-function-theorem sensitivitiesdx*/dθ = −(∂f/∂x)⁻¹ ∂f/∂θfrom the fixed-point Jacobian. Continuation scans (reset_conc=0) carry a θ-dependent seed and would need sensitivity seed-chaining across points. Recommend supporting parity / reset-to-seed dose-response first and keeping Newton/continuation gradient-free with a clear message.‖f(t,y)‖₂ / n, not the sensitivity residual.dx_ss/dθcan converge on a slower timescale thanx_ssin stiff systems, so the recovered tensor at the truncation row may be under-converged and yield subtly wrong gradients. Validate the recovered tensor against finite differences, and/or also bound the sensitivity residual before truncating.Validation
Add a test comparing the assembled dose-response objective gradient against a finite-difference gradient of the same objective, on a small parity/steady-state dose-response fit.