Skip to content

feat(gradient): route free params that reach the model through a condition (#511) - #513

Merged
wshlavacek merged 1 commit into
mainfrom
feat/gradient-per-condition-estimated-ic
Jul 23, 2026
Merged

feat(gradient): route free params that reach the model through a condition (#511)#513
wshlavacek merged 1 commit into
mainfrom
feat/gradient-per-condition-estimated-ic

Conversation

@wshlavacek

Copy link
Copy Markdown
Collaborator

Closes #511.

Problem

An edition-2 gradient fit aborted whenever a free parameter reached the model only through a condition: parameter-reference perturbation (a per-condition estimated initial condition, ADR-0076). route_experiment refused any is_param_ref mutation, so the fit fell back to a gradient-free optimizer:

gntr: Error: Condition sets 'init_b10' to the value of free parameter 'init_b10_1' … The gradient path cannot yet route a free parameter that reaches the model through a condition target of a different id

That is a common PEtab pattern, so problems like Bruno_JExpBot2016 silently lost the gradient path entirely.

Approach

Compose the chain rule instead of refusing. A condition assignment target = free_param gives the referenced free parameter a RouteContribution on the target's own sensitivity column — the IC axis when the target bares a species initial value (d(IC)/d(target) = 1), the parameter axis for an ordinary global such as a shared rate multiplier.

The structural piece the issue's suggested direction did not anticipate: one free parameter may be assigned to several targets in a single condition (Bruno's szea drives six rate multipliers at once), so its derivative is a sum of native columns. ParamRoute therefore becomes a list of contributions rather than a single (target, key, factor), and both assembly accessors — objective and constraint — sum them, so the gradient, residual Jacobian and EFIM/Fisher block all follow.

Supporting changes:

  • Both backends expose a bare-initializer seed map ({model param -> species}) via sensitivity_entity_namespace, which now returns a 3-tuple — keeping the routing core backend-agnostic.
  • _setup_gradient_path applies the union request over the wildtype and every scored condition (apply_routings). The wildtype alone is not a superset once a condition routes a free parameter to a column no other experiment binds; an extra requested column is harmless, a missing one aborts the assembly.

Boundaries kept as honest refusals

Rather than emitting a silently-zero or wrong column, GradientNotSupported is still raised for a seed whose d(IC)/d(param) is not a plain 1 (non-bare initialAssignment, an amount species needing a non-unit concentration factor, or a parameter seeding several species), a non-= parameter reference, and a condition target that binds no sensitivity entity.

Validation

Against the real Bruno_JExpBot2016 job (13 free params on log10 scale, 6 conditions):

Check Result
Assembled gradient vs central finite differences, all 13 params worst rel. err 2.2e-07
gntr fit vs Grein et al. reference J* (threshold 1.92) OG = 1.1e-05 → SOLVED
Recovered vs PEtab nominalValue vector worst rel. diff 1.8e-06
Full test suite (with BNG2.pl) 3298 passed, 0 failed, 0 errors

The FD oracle covers both composed axes — the initialAssignment-seeded IC and szea's six-way multiplier sum. A problem that previously could not use the gradient path at all now solves it essentially exactly, and J* was independently confirmed against best_fx_marvin.csv.

Tests

New coverage for: IC and parameter composition, the multi-target sum, all three refusal paths, classify_condition_target, the union request (apply_routings), constraint + Fisher blocks with summed routes, non-unit condition factors composed with a param-ref, net-backend composition end-to-end, and SBML seed-map exposure incl. the non-bare and amount-species refusals. Plus an FD oracle on a synthetic mini-Bruno.

Notes

  • sensitivity_entity_namespace returning a 3-tuple is a breaking change for any external caller; all in-tree call sites are updated.
  • Not covered: an FD oracle on the net backend (its routing is verified end-to-end; the assembly math is shared and FD-verified on SBML), and pre-equilibration × param-ref (already refused at config level).

🤖 Generated with Claude Code

…ition (#511)

An edition-2 gradient fit aborted whenever a free parameter reached the model
only through a `condition:` parameter-reference perturbation (a per-condition
estimated initial condition, ADR-0076) -- `route_experiment` refused any
`is_param_ref` mutation, so the fit fell back to a gradient-free optimizer.
That is a common PEtab pattern, so problems like Bruno_JExpBot2016 silently
lost the gradient path entirely.

Compose the chain rule instead of refusing. A condition assignment
`target = free_param` now gives the *referenced* free parameter a
RouteContribution on the target's own sensitivity column: the IC axis when the
target bares a species initial value (d(IC)/d(target) = 1), the parameter axis
for an ordinary global such as a shared rate multiplier.

Because one free parameter may be assigned to several targets in a single
condition (Bruno's `szea` drives six rate multipliers at once), a ParamRoute
becomes a *sum* over contributions rather than a single (target, key, factor).
Both assembly accessors -- objective and constraint -- sum them, so the
gradient, the residual Jacobian and the EFIM/Fisher block all follow.

Two supporting changes:

* both backends expose a bare-initializer seed map (`{model param -> species}`)
  through `sensitivity_entity_namespace`, which now returns a 3-tuple, keeping
  the routing core backend-agnostic; and
* `_setup_gradient_path` applies the UNION request over the wildtype and every
  scored condition (`apply_routings`). The wildtype alone is not a superset once
  a condition routes a free parameter to a column no other experiment binds --
  an extra requested column is harmless, a missing one aborts the assembly.

Correctness boundaries stay honest refusals rather than silently-zero columns:
a seed whose d(IC)/d(param) is not a plain 1 (non-bare initialAssignment, an
amount species needing a non-unit concentration factor, or a parameter seeding
several species), a non-'=' parameter reference, and a condition target that
binds no sensitivity entity.

Validated on the real Bruno_JExpBot2016 job: the assembled gradient matches
central finite differences to 2.2e-07 across all 13 free parameters, and a gntr
fit now solves the problem (optimality gap 1.1e-05 against the Grein et al.
reference J*, threshold 1.92), recovering the PEtab nominal parameter vector to
1.8e-06 -- a problem that previously could not use the gradient path at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wshlavacek
wshlavacek merged commit 5150ba7 into main Jul 23, 2026
8 checks passed
@wshlavacek
wshlavacek deleted the feat/gradient-per-condition-estimated-ic branch July 23, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant