Skip to content

Support gradient-based fitting to parameter_scan (dose-response) objectives — reuse the per-point sensitivities already computed and discarded #476

Description

@wshlavacek

Summary

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 GradientNotSupportedPybnfError (pybnf/gradient/assembly.py:266-270pybnf/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.
  • SBML/Antimony twin: _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 (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

  1. 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.
  2. 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.
  3. Compose with Gradient fit aborts on incidental non-differentiable actions (unscored parameter_scan / non-ODE simulate) #475 so incidental (unscored) scans don't interfere with the fit.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions