fix: gradient-fit a piecewise model with a count likelihood#522
Merged
Conversation
Two independent defects made `fit_type = lbfgs` (or any gradient fit) score `inf` or `nan` everywhere on a model built from `if()`-gated rate laws with a negative-binomial count likelihood -- the shape of every COVID-19 compartmental job in the corpus (Lin 2021, Mallela 2024). 1. Over-requested output-sensitivity selectors. `_extract_output_sensitivities` asked bngsim for an `expression:` sensitivity for EVERY global function whenever `print_functions` was on. bngsim differentiates function bodies symbolically and refuses, per function, any body carrying a non-differentiable construct -- `if()`, a comparison, min/max/abs/floor, a table function (lanl/bngsim#198) -- raising `ValueError` if such a selector is requested. `has_sensitivities_expressions` only says "an expression block exists", so it could not stand in for "this expression is differentiable". A piecewise model has every function refused, so every simulation died and the objective was `inf` at every point. Filter the request to the differentiable expressions (`_differentiable_expression_names`); a scored column is essentially never one of these, and if it were, its absence now surfaces as a clean gradient error rather than a dead simulation. No support map (older bngsim, a Result loaded from disk) => unchanged behavior. 2. Divide-by-zero in the negative-binomial derivatives. `d(data_fit)/d(mean) = r (mean - obs) / (mean (r + mean))` is `0/0 -> nan` (observed zero) or `-inf` at a MEAN-centered prediction of exactly 0. That is not a pathological point: any model gated off over part of the fit window predicts exactly zero there -- an epidemic model before its start time t0 -- and `data_fit` scores it finitely by clipping `prob = r/(r+mean)` at `1 - 1e-10`. The `nan` propagated out to the optimizer, which then tried to assign a free parameter `nan`. Mirror the value path's clip on the mean side (`_mean_for_slope`) so every expression that divides by the mean gets the finite slope belonging to the objective actually being scored. MEDIAN centering never reaches the floor. Verified end to end on the Mallela et al. 2024 NYC job (42 species, 88 reactions, 14 `if()` functions, 649 daily counts, neg_bin with a fixed dispersion): before, every start returned `inf`; after, L-BFGS-B converges to nll 5235.80, improving on the published MAP's 5285.16 in the same box.
wshlavacek
added a commit
to wshlavacek/BNGL-Models
that referenced
this pull request
Jul 26, 2026
The two piecewise-gradient fixes these jobs depend on are now on PyBNF main: lanl/PyBNF#522, merged as 7913d037. Link the PR from each slug's VALIDATION.md (Toolchain requirement) and README (Run), and from the paper-level README, instead of naming the commit subject. Re-verified against merged main with no local patch on the path: the nyc fit still converges to nll 5235.799376765947.
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 free
to 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.
Two independent defects made any gradient fit (
lbfgs/trf/gntr) scoreinfornaneverywhere on a model built from
if()-gated rate laws with a negative-binomial count likelihood— the shape of every COVID-19 compartmental job in the corpus (Lin 2021, Mallela 2024). Neither is
reachable from a
.conf; both are ordinary bugs.1. Over-requested output-sensitivity selectors
BngsimModel._extract_output_sensitivitiesasked bngsim for anexpression:sensitivity forevery global function whenever
print_functionswas on. bngsim differentiates function bodiessymbolically and refuses, per function, any body carrying a non-differentiable construct —
if(),a comparison,
min/max/abs/floor, a table function (lanl/bngsim#198) — andResult.output_sensitivitiesraisesValueErrorif a refused function is among the requestedselectors.
has_sensitivities_expressionsonly says "an expression block exists", not "every expression isdifferentiable", so it could not stand in for the per-function verdict. A piecewise model has
every function refused — all 14 of the Mallela 2024 model's are
if()chains — so everysimulation died with
ValueErrorand the fit's objective wasinfat every point.The request is now filtered to the differentiable expressions
(
_differentiable_expression_names, reading bngsim's per-function verdict map). Notes:OutputSensitivities.slice_for), neverpositionally against the
Datacolumns, so dropping a selector is safe;stack_scan_sensitivitiestakes selectors from the first dose point and every point shares them(the verdict is a property of the model, not the point);
observable. If one ever were, its absence now surfaces as a clean "no sensitivity column"
gradient error instead of a dead simulation;
Resultloaded from disk) ⇒ nothing to filter on ⇒behavior is exactly as before;
The SBML path (
bngsim_sbml_model.py) requests onlyspecies:selectors and is unaffected.2. Divide-by-zero in the negative-binomial derivatives
d(data_fit)/d(mean) = r (mean − obs) / (mean (r + mean))is0/0 → nan(observed zero) or−infat a MEAN-centered prediction of exactly 0. That is not a pathological point: any model gated
off over part of the fit window predicts exactly zero there — an epidemic model before its start
time
t0, a stimulus-driven readout before the stimulus — anddata_fitscores such a pointfinitely by clipping
prob = r/(r+mean)at1 − 1e-10. Thenanpropagated through thegradient and out to the optimizer, which then tried to assign a free parameter
nan:_mean_for_slopenow mirrors the value path's clip on the mean side (prob ≤ 1 − _PROB_CLIPisexactly
mean ≥ r · _PROB_CLIP/(1 − _PROB_CLIP)), so every expression that divides by the meangets the finite slope belonging to the objective actually being scored. MEDIAN centering never
reaches the floor (
_mean_for_medianis strictly positive), so it is a no-op there.location_fisherdeliberately keeps itsmean <= 0 → 0.0guard rather than adopting the floor,and now says why: the true
I_meandiverges as1/mean, so flooring it would put a~1/(r·_PROB_CLIP)weight on every gated-off row and let those rows dominate the EFIM, crushingthe
gntrtrust-region step. Zero — "this row carries no information about the mean" — is thestable reading, and the gradient (which does floor) supplies the direction.
Verification
main).no support map) and three for the count family at a zero prediction (finite slope and dispersion
gradient for observed 0 / 1 / 4212; and the slope equals the un-clipped closed form at the very
mean the value path's clip already pretends to score, so value and gradient stay consistent).
if()functions,649 daily counts,
neg_binwith a fixed dispersion): before, every start returnedinf; after,L-BFGS-B converges to nll 5235.80, improving on the published MAP's 5313.95 in the same
box. The same holds for all four MSAs in that paper — see
pybnf-jobs/Mallela-2024/*/VALIDATION.mdin the BNGL-Models corpus.Follow-up (not in this PR)
bngsim exposes the per-function verdict only as
Result._expression_sens_support; a publicaccessor upstream would let this filter drop the private-attribute
getattr.