Epic / tracker. Surface objective gradients and least-squares residual Jacobians
w.r.t. free parameters by consuming BNGsim's output_sensitivities tensor, so PyBNF can host
gradient-based optimizers (#386). This issue tracks the work; the implementation ships as the
dependency-ordered child issues below, each independently verified by a finite-difference check
against PyBNF's own loss(θ).
Backend status — VERIFIED (bngsim 0.10.14, 2026-06-27)
bngsim.capabilities()["features"]["output_sensitivities"] → True.
Result.output_sensitivities(selectors, *, axis="parameter") returns ∂g/∂θ in native
parameter space (no log/scale transform — PyBNF owns that, ADR-0029).
Simulator(..., sensitivity_params=, sensitivity_ic=, sensitivity_method=) accepts the request.
- No further BNGsim work is needed for the deterministic-ODE path.
Scope correction (edition-2)
This epic is edition-2 only (edition >= 2), and the parameter-mapping is not the classic
__FREE/__REF story the original draft used. Edition-2 binds free parameters to the model
by id (ADR-0034) and expresses per-condition values as Mutation(var, operation, value)
perturbations (ADR-0028). So "per-condition pinning" is just the perturbation operator's local
derivative: = → zero that column; *c//c → chain-rule factor; +/- → factor 1. No
surrogate recovery. Forward sensitivities are deterministic-ODE only — network-free/stochastic
models are never candidates.
Convention pin (the reconciliation the optimizer must trust)
PyBNF's Gaussian loss is Σ r²/(2σ²), r = pred − obs. Define the least-squares residual
ρ_i = (pred_i − obs_i)/σ_i. Then loss = ½‖ρ‖², residual-Jacobian J_ij = (1/σ_i)·∂pred_i/∂θ_j
(θ native), and ∂loss/∂θ = Jᵀρ (not 2Jᵀρ). scipy.least_squares minimizes ½‖ρ‖² with the
same ρ/J, so the residual form and the scalar form agree by construction. Sampling-space applies
J → J·diag(∂θ/∂u) once, via priors/scale.py (ADR-0029).
Cut 1 — usable-gradient MVP (strict order)
Coverage layers (independent; each adds one FD-oracle case; not yet opened)
Backward compatibility
Purely additive and opt-in. The scalar (non-gradient) execution path is byte-unchanged when the
gradient path is inactive. Metaheuristic fits are unaffected.
Original issue text (pre-epic, superseded by the children above)
The original draft framed item 4 around the classic __REF surrogate-base split (ADR-0027);
that framing is superseded by the edition-2 bind-by-id + perturbation-operator mapping described
above and implemented in #448. The backend re-scoping ("What BNGsim ships today") is confirmed
against bngsim 0.10.14.
Epic / tracker. Surface objective gradients and least-squares residual Jacobians
w.r.t. free parameters by consuming BNGsim's
output_sensitivitiestensor, so PyBNF can hostgradient-based optimizers (#386). This issue tracks the work; the implementation ships as the
dependency-ordered child issues below, each independently verified by a finite-difference check
against PyBNF's own
loss(θ).Backend status — VERIFIED (bngsim 0.10.14, 2026-06-27)
bngsim.capabilities()["features"]["output_sensitivities"]→True.Result.output_sensitivities(selectors, *, axis="parameter")returns∂g/∂θin nativeparameter space (no log/scale transform — PyBNF owns that, ADR-0029).
Simulator(..., sensitivity_params=, sensitivity_ic=, sensitivity_method=)accepts the request.Scope correction (edition-2)
This epic is edition-2 only (
edition >= 2), and the parameter-mapping is not the classic__FREE/__REFstory the original draft used. Edition-2 binds free parameters to the modelby id (ADR-0034) and expresses per-condition values as
Mutation(var, operation, value)perturbations (ADR-0028). So "per-condition pinning" is just the perturbation operator's local
derivative:
=→ zero that column;*c//c→ chain-rule factor;+/-→ factor 1. Nosurrogate recovery. Forward sensitivities are deterministic-ODE only — network-free/stochastic
models are never candidates.
Convention pin (the reconciliation the optimizer must trust)
PyBNF's Gaussian loss is
Σ r²/(2σ²),r = pred − obs. Define the least-squares residualρ_i = (pred_i − obs_i)/σ_i. Thenloss = ½‖ρ‖², residual-JacobianJ_ij = (1/σ_i)·∂pred_i/∂θ_j(θ native), and
∂loss/∂θ = Jᵀρ(not2Jᵀρ).scipy.least_squaresminimizes½‖ρ‖²with thesame
ρ/J, so the residual form and the scalar form agree by construction. Sampling-space appliesJ → J·diag(∂θ/∂u)once, viapriors/scale.py(ADR-0029).Cut 1 — usable-gradient MVP (strict order)
sensitivity_params/sensitivity_icrouting (bind-by-id; condition operator chain-rule) — B — Gradient plumbing: route free params to sensitivity_params/sensitivity_ic (bind-by-id + condition chain-rule) #448Coverage layers (independent; each adds one FD-oracle case; not yet opened)
−r²/σ³ + 1/σ, σ not in tensor)bngsim_sbml_model.py:618).con/.prop)Backward compatibility
Purely additive and opt-in. The scalar (non-gradient) execution path is byte-unchanged when the
gradient path is inactive. Metaheuristic fits are unaffected.
Original issue text (pre-epic, superseded by the children above)
The original draft framed item 4 around the classic
__REFsurrogate-base split (ADR-0027);that framing is superseded by the edition-2 bind-by-id + perturbation-operator mapping described
above and implemented in #448. The backend re-scoping ("What BNGsim ships today") is confirmed
against bngsim 0.10.14.