Repository navigation
parity: each model through every path that should give the same answer, and a tolerance sweep in fd_sens (#1257) - #1291
Merged
Conversation
…lanl#1257) An analytic column that still moves between rtol 1e-10 and 1e-12 where the trajectory settles is a DIFF, whatever the difference of plain runs said: before lanl#1242, triangle_clamp_narrow's k_clock column moved 107% there, and the difference alone passed it. Asked only of the columns the verdict judges, above the solver's resolution. Eight pulses over the clock, max/abs, clamp and if(), narrow and wide, join the corpus.
lanl#1257) A sensitivity run's state against a plain run's, the FD Jacobian against the analytic one, KLU against dense, compiled against interpreted, cc against MIR, a comma-decimal locale against C; and three rules for bugs whose answers are right: a sensitivity run that does not complete, a sparsity pattern that misses entries, a backend other than the one asked for. Shape models run on every pull request (python/tests/test_differential_shapes.py); the corpus runs nightly beside the parity suites, against a baseline.
|
This rocks dude! |
lanl#1257) The answer must not depend on where it is read: 8x the output points, read at the base's own sample times, is the base. Before lanl#1263 it found lanl#1244's pulse train 7.5x the trajectory's peak off. The shapes run on the MIR leg too, the one place cc against MIR and codegen=False on a MIR build both run. The nightly's per-model cap is 900 s: nine models passed 300 s on the first CI run, compiling cold. lanl#1214's xfail goes, fixed by lanl#1224.
…s KLU and cc (lanl#1257) The sweep on the four narrow pulses runs with the parity tests, not only the nightly: before lanl#1242 the clamp ones' k_clock columns moved there. lanl#1254's xfail holds only where KLU is built: the MIR leg builds without it, skips the check, and read that as an unexpected pass. A machine with no C compiler skips the cc paths instead of reporting them as crashes.
…ial-harness # Conflicts: # .github/workflows/mir.yml # ci/test_floors.env
This was referenced Oct 26, 2026
…erpreter is a finding (lanl#1257) klu_vs_dense left its "dense" reference to the automatic rule, which sends a model of 50 species or more with a sparse Jacobian to KLU, and the base with it: on those models the check was KLU against KLU (a 60-species chain reads linear_solver 1 for the base, jacobian="fd" and the forced-sparse run alike). 99 of the BioModels under 1 MB route that way. The reference is now force_dense_linear_solver=True on the same Jacobian. It reads DIFF on BIOMD0000000338 and 339 at once (3.5% and 1.8% of the peak; own drift 1e-7): past a discontinuity where CVODE's step collapses, KLU (their default route) and dense take different branches. On 338 KLU ends Fg at 310.989 at every rtol from 1e-6 to 1e-12; dense and libRoadRunner end it at 0.73-0.75, drifting with the tolerance, and dense lands on KLU's branch at 1e-10. Which branch is right is open; that the two solvers disagree is what the check is for. backend_as_asked could not fail on its codegen=True half: _compiled read a codegen=True run on ExprTk as "no compiler" and the rule as SKIP, while cc_vs_interp compared the interpreter with itself. codegen=True builds cc or MIR or raises, so ExprTk there is the finding. A missing compiler is now recognized as bngsim reports it off Windows too (a cc that is not there, macOS's cc shim without the Command Line Tools). cc_vs_mir checks its cc arm ran cc. A tenfold tighter run that does not complete no longer blanks the model as no_base: its comparisons are SKIP, said why, and the rules that compare no answers still run (MODEL1112050001). The sparsity rule leaves out a state its one-interval run does not reach, as it leaves out a non-finite one. A trajectory wholly under the absolute floor (1e3·atol: 16 BioModels in molar units) read PASS whatever a path gave; a path inside the floor there is SKIP, said why, and one past it is still judged. The sparsity rule steps a state on its own scale: an absolute 1e-3 floor stepped a nanomolar state by its whole size and hid a missing slot as unresolvable. cc_vs_interp and backend_as_asked share their two runs. The shapes module removes its temp directory. Docs: 53 fd_sens models, 10 checks, the grid check.
…ale (lanl#1257) The sweep's two sensitivity runs at rtol 1e-10 and 1e-12 came after the difference's, which may spend 90% of the job's cap, with no deadline: a slow sweep ran the job into the scheduler's kill, a TIMEOUT where a verdict had been reached. Each run is now bounded by the same deadline, and a sweep out of time fails as a sweep and leaves the verdict. A column was asked to clear an absolute 1e-14 (1e-14 of max(peak, 1)), in the units of state per parameter: nanomolar_dimer's two columns, 1e-28 and 6e-20 per unit of parameters of 2e18 and 4e9, and 17% of its 1.5e-9 peak per relative change, were both left out, and the sweep judged nothing there. A column is now |p|·dx/dp against 1e-14 of the state's peak. All 106 fd_sens cases still PASS locally, now with a judged column on every settled model.
This was referenced Oct 27, 2026
) The review commits change what the check reports, on the nightly's own runner: - 16 BioModels whose trajectories sit wholly under the absolute floor (1e3·atol) read SKIP, said why, on the six comparisons (15 on sens_vs_plain), where they read a PASS that could not fail. - BIOMD0000000339 klu_vs_dense reads DIFF, 1.79e-2 of the peak, now that the reference is dense; before, it read exactly 0, KLU against KLU (lanl#1304). On this runner BIOMD0000000338's dense run lands on KLU's branch (1.12e-4, PASS). - MODEL1112050001 and MODEL1112110002, whose tenfold tighter run fails, keep their rules that compare no answers (sparsity, backend, sens_runs) instead of reading SKIP on every row. - MODEL1006230049, 053 and 077 finish inside the 900 s cap (TIMEOUT to PASS, SKIP on cc_vs_mir), with cc_vs_interp and backend_as_asked sharing two runs. fd_sens.json is unchanged: its job ran clean against it.
This was referenced Oct 27, 2026
This was referenced Oct 27, 2026
Montana
added a commit
to Montana/bngsim
that referenced
this pull request
Oct 27, 2026
lanl#1291's review commits re-recorded the differential baseline (run 40440170341) under the forced-dense klu_vs_dense. The merged baseline is theirs for every existing row, plus this PR's 1,353 jacobian_values rows from run 40448597362, with the tally recomputed; nothing else differs from lanl#1291's. The runner's docstring takes both edits (lanl#1291's 8x-grid check, this PR's fourth rule).
This was referenced Oct 27, 2026
wshlavacek
added a commit
to Montana/bngsim
that referenced
this pull request
Oct 28, 2026
…l#1254's xfail The colored quotient's session pointer was a second field beside main's dq_cvode_mem, which lanl#1002 added for exactly this, and it was set at the two CVodeSetUserData sites rather than where the callback is chosen. It is now dq_cvode_mem, set in setup_linsol_and_jac's colored branch as the dense branch sets it. min_inc is formed by one helper, cvode_dq_min_inc, which the dense quotient also uses; it returns false where CVodeGetCurrentStep fails, where the colored callback used to carry on with h = 0 and step a species at 0 by 0. lanl#1291's strict xfail on equality-dwell / klu_vs_dense is dropped: this is the fix it was waiting for.
wshlavacek
added a commit
to Montana/bngsim
that referenced
this pull request
Oct 28, 2026
…a26f5b (lanl#1302, lanl#1304) verdicts.json of scheduled run 40712357505 on main at 9a26f5b, byte for byte. Three cases move, and no others: - BIOMD0000000338|klu_vs_dense PASS -> EXCEPTION. Since lanl#1302, the forced-dense jacobian="fd" leg fails at t = 0.72 (CV_CONV_FAILURE) on every Linux runner; the KLU leg completes (ajac_vs_fdjac, the same KLU run bit for bit, passes). The model is lanl#1304's. - BIOMD0000000385|ajac_vs_fdjac and |klu_vs_dense TIMEOUT -> PASS. The old file is lanl#1291's run, where the pool killed every row at the 900 s cap; with lanl#1321's per-check deadlines and the 1500 s cap these two finish.
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.
Closes #1257, scoped as the maintainer review asked (#1257 (comment)).
What it does
A recurring bngsim bug is two code paths that should give the same answer and don't (#1182, #1227, #1228, #1235, #1254). This runs those probes for every model, every time:
sens_vs_plainajac_vs_fdjacjacobian="fd"jacobian="auto"klu_vs_denseforce_sparse_linear_solver=Truecc_vs_interpcodegen=Truecodegen=Falsecc_vs_mirBNGSIM_CODEGEN_JIT=mirlocalede_DE.UTF-8, in a child processIt also applies three rules for bugs whose answers are right, which no comparison of answers sees:
sens_runs: the sensitivity run doesn't complete where the plain run does (The issue #952 hold check refuses a sensitivity run at a bend: a continuous narrow pulse (max/abs or if()) is refused at rtol <= 1e-10, blamed on a jump it does not have #1229). Reported as UNSUPPORTED; the baseline turns a new one into a finding.sparsity_covers_jacobian: every entry that central differences of the RHS find nonzero has a slot injacobian_sparsity(Jacobian sparsity has no edges for a rate_of__ token, so a written-out rate declines the analytic Jacobian (#1124) #1239).backend_as_asked:codegen=Falseruns the interpreter andcodegen=Trueruns compiled code (On a MIR build, Simulator(method='ssa', codegen=False) still JITs the propensities: the C++ selector reads only BNGSIM_SSA_NO_CODEGEN #1213).Where it lives, per the review
fd_sensgains the tolerance sweep: an analytic column that still moves between rtol 1e-10 and 1e-12 where the trajectory settles is a DIFF. It also gains eight pulse models over the clock (max/abs,clamp,if(); narrow w = 0.01 and wide w = 0.5). Of the proposal's five rules,fd_sensalready had three (the relative step, step agreement, noise floors). The sweep supplies rules 3 and 5, and it judges only the columnsfd_sens's floors judge.parity_checks/differential/_differential.py.python/tests/test_differential_shapes.py, 158 tests in about 7 s, on every PR. It includes the shapes the proposal promised to add: an event at a sample time, a moving compartment,rateOfacross compartments, and an event-set pulse train. Known open bugs are strict xfails naming their issue: An exact equality on a state, if(Bobs==0, kb, 0), is silently wrong on KLU with the FD Jacobian: Y(10) off by 1e-3 at rtol 1e-8 and 4.7e-3 at 1e-10, where dense and the analytic Jacobian are right #1254 (KLU with FD on an equality) and log10(p) or log2(p) in a derived rate constant declines the analytic sensitivity RHS for the whole model, which falls back to the difference quotient #1214 (log10declines the analytic sensitivity RHS).differential_run.pywrites a_corereport, one row per (model, check), compared bycompare.shagainstbaselines/differential.jsonand the previous night.fd_sens's 53 models and the BioModels under 1 MB, from the corpus job's cache.locale-gen de_DE.UTF-8. The runner checks for a comma-decimal locale once and exits 2 without one, so the missing report alerts.How the oracle stays careful
Every false positive seen so far came from the checking, not from bngsim. A disagreement counts only past
max(1e3·rtol, 10 × the base's own drift at a tenfold tighter tolerance). Each cell is judged on the trajectory's own scale, past an absolute floor of the run's atol.Running over the larger corpus first, as asked, changed four things:
mwc(itsphicolumn) and the narrow pulses' position and width. Those columns are truly 0 and hold only the integration's error.rr_odeandsbml_semantic, so it's SKIP here; a PASS turning into SKIP still alerts.The pytest file holds the gate to account both ways. Lorenz over t = 25 has an FD-Jacobian path 3.4e-4 from the base, past the 1e-5 that 1e3·rtol allows, and both are right. With the gate it isn't a finding; without it, it is.
Tests
python/tests/test_differential_shapes.py:parity_checks/tests/test_differential_run.py: rows and outcomes, a model with no rows, the manifests, the BioModels size cap, the locale stop, an unknown check.parity_checks/tests/test_fd_sens.py: a moving column, an unsettled trajectory, the rounding floor, judged columns only, a converged column, and how the sweep's verdict applies.Mutations. The library: 9 of 9 caught.
The sweep: 4 of 4 caught (no settled gate, judged columns ignored, no rounding floor, sweep never applied).
Negative control on #1227. Against the commit just before #1242,
fd_senswith the sweep flagspulse_triangle_clamp_narrow(k_clockmoves 107%) andpulse_ramp_clamp_narrow(1.15e-3). Without the sweep, all 16 pulse rows pass. On main, all 16 pass with it.Suites.
parity_checks/tests: 921 passed, 18 skipped. The CI-config tests, the skip audit and the shapes: 295 passed, 2 xfailed. ruff is clean on everything touched.False positives on the larger corpus
3,437 models, 30,933 rows: every
.netinbenchmarks/models/net, the 214 SBML event models, the 1,823 semantic cases and the 1,323 BioModels. 23,361 PASS, 7,288 SKIP, 217 UNSUPPORTED, 36 TIMEOUT (4 models at the 300 s cap), 12 EXCEPTION, 19 DIFF.One model is a false positive: MODEL7743528808, 2 rows. Its paths are 2.15e-3 apart against a bound of 2.1e-3, and the difference is gone at a tenfold tighter tolerance. I left the gate as it is rather than fit it to one model. The case would sit in the baseline and never alert.
What it found on main
jacobian="fd"alone refused: "force_sparse_linear_solver: … Drop the flag", a flag never set. The automatic routing picks KLU, and the pattern has no nonzeros to color.Since opening
mir.yml's legs, the one place a MIR build exists, which also exercises On a MIR build, Simulator(method='ssa', codegen=False) still JITs the propensities: the C++ selector reads only BNGSIM_SSA_NO_CODEGEN #1213's check (codegen=Falseon a MIR build). A missing C compiler is a skip there, not a crash.parity_checks/tests/test_fd_sens.py), not only nightly.fd_senscases, each comparing clean against it.Compatibility
python/bngsimchanges, so no changelog fragment is required (ci/changelog.py required).ci/test_floors.env: the floors move with the added tests, as the file asks. Python is 16,137 on main, 16,271 here; floor 16,261. Parity is 921 on main, 939 here; floor 929.baselines/differential.jsoncomes from this PR's own nightly run (ALLOW_MISSING_BASELINEis set on pull requests), andfd_sens.jsonis refreshed from it for the eight new models. Until then they are listed as new, not alerted.Performance
fd_sens: two more sensitivity runs per job for the sweep.Remaining risks
_sparse_declinedand codegen's "declined this model" are recognized by their text: bngsim raises no type of its own for them. A reworded message would turn a SKIP or UNSUPPORTED into an EXCEPTION, which alerts.cc_vs_mirruns only on a MIR build; the nightly's runner has none, so it reads SKIP there.Maintainer review: the three strongest objections
fd_sens." It doesn't touch sensitivities against differences: those stay infd_sens, which gains the sweep. The new suite compares bngsim's paths with each other, which no existing suite does.SparseJacobianUnavailabletype and a codegen-declined type would make it exact. That's a small follow-up I can open.Diff (
fd_sens_run.py, the one existing runner this changes; the new files are listed above)