Repository navigation
steady state: the run test retries a NaN f at the clamped state, as the time course does (#1357) - #1370
Conversation
…n differences (lanl#1357) On the compiled right-hand side, glioblastoma_egfrviii's march came to a state at residual 2.1e-39, its rounding a few ulps from the interpreter's. The scaled criterion's run test from there stopped at t = 3.9e5 on the closed-form Jacobian, which is the interpreter's to the bit, and the state was refused with a run measure of inf. The solve's own retry on difference quotients (lanl#127) runs before the run test, and the run test had none of its own. Under jacobian="auto" with a closed form installed, a run test that comes back inf is now taken once more on differences, as the hold run already is. The state returned is still the solve's, so solver_jacobian_retried is not set; jacobian="analytical" still surfaces the failure.
…he time course does (lanl#1357) glioblastoma_egfrviii raises AKT(s~P) to 3.5. The compiled march ends with it at exactly 0 on x86-64 Linux (within 1e-52 on arm64 macOS), and the run test's first corrector iterate takes it an ulp below 0, where the power is NaN. CVODE then fails on either Jacobian, and a state at residual 2e-40 was refused. The difference-quotient retry of the run test only stepped around this on macOS; it is replaced. The run test now takes f again at the nonnegative-clamped state where f is not finite (GH lanl#135), over the slots lanl#706 allows, which move into bngsim/nonneg_clamp.hpp for both integrators. The march, the take-on run and the closed-form Jacobians are unchanged.
…ompiled-march-refusal # Conflicts: # src/cvode_simulator.cpp # src/steady_state.cpp
…clamped state too (lanl#1357) CVODE takes the Jacobian at its prediction. From where the compiled march ends on x86-64 Linux, AKT(s~P) at exactly 0 with f = -1.1e-39, the first prediction puts it near -1e-43, where d/dx x^3.5 = 3.5*x^2.5 is NaN, and the run test still failed at t = 0. Run from those bits through a temporary hook, the closed-form run test measured inf without this and 1.4e-16 with it.
|
Thanks. We reviewed this independently, Opus 5.5 and Fable 5.1, each on our own build of 7bb8fb1 (on main c0877fa; the merge is a fast-forward, tree 3584813b), macOS arm64, KLU and MIR on. Main has since moved to e425546 (#1315, #1325), which touches none of this PR's files; the merge is clean. The fix does what it says:
We found no defect in the diff. There is one pre-existing gap the issue asks about, one merge-order point with #1346, one test that fails on a KLU-off build, and lows. What we checked comes first, so it needn't be redone. What we checked
1. The issue's second ask still stands#1357 also says the closed form's failure in the run test "does not retry on difference quotients … Whatever fails here is not recorded as
The clamp can't help here, because the state is already at 0. The time course survives by falling back to the quotient (GH #176). One more run test on 2. Land with #1346 in mind: the run test's clamp is a copyThe new header moves the mask, not the clamp. With #1346 rebased this morning (d787eac), we built main e425546 + this PR + #1346. Both PRs' test files pass together: 234 passed, 3 skipped (MODEL1812040005 not materialized here), under cc and MIR. And the asymmetry shows up on #1346's own model shape:
It's the same in either species order. P moves 1e-24 over the run, far inside its tolerance. One clamp and one reading in 3.
|
|
A note for whoever merges this: #1350 (#1217) lands first, and this PR has to undo one of its marks. #1350 strict-xfails Once #1350 is in main, this PR should:
Check: |
… clamp and one reading (lanl#1357) lanl#1350 (merged first) strict-xfails test_a_march_whose_integrator_fails_returns_the_state_it_passed[integration] under a lowered BNGSIM_CODEGEN_THRESHOLD, citing lanl#1357. This branch fixes lanl#1357, so merged with main the case passes there, and the forced run reported "[XPASS(strict)] lanl#1357" and failed. No workflow lowers the threshold, so CI could not show it. The mark goes, and with it xfail_when_threshold_forced, which nothing else uses. Under BNGSIM_CODEGEN_THRESHOLD=1 the file now gives 227 passed, 3 skipped. test_a_run_test_from_a_fractional_power_at_zero_is_taken_to_its_time [fd colored] asserts the solve factored with KLU. On a KLU-off build, which conftest declares a supported configuration ("KLU not compiled"), route_to_sparse_linear_solver is false whatever is forced, so it failed there. It now skips with that reason, as test_lu_fill_routing.py does. nonneg_clamp.hpp exists so the two integrators retry by one rule, but two parts were still written twice: - read_values and ClampReading (finite / NaN only / infinite) move into the header, and the run test asks read_values == nan_only rather than carrying ss_nan_and_no_inf, a second copy of the same loop. - clamp_nonneg_into takes the time course's optional spare slots, and clamp_state_nonneg calls it, so the clamp itself is defined once. Neither changes what either integrator computes.
Closes #1357.
What was wrong
steady_state(method="integration")onglioblastoma_egfrviii_seed.net, compiled (codegen=True), on main:continued_run_distanceThe interpreted solve converges on both platforms.
What it was
The model's
v_cancer_driveraisesTotal_pAKT, which is AKT(s~P), ton_hill = 3.5. A fractional power of any negative number is NaN.On x86-64 Linux, the compiled march ends with AKT(s
P) at exactly 0. On macOS it ends within 1e-52 of it. The scaled criterion's run test (#1001) then takes that state on for 1e6. The corrector's first iterate puts AKT(sP) an ulp below 0, f is NaN, and CVODE fails on any step size. I measured this on main's core:Model.rhsis finite at AKT(s~P) = 0 and 1e-60. It is NaN at -1e-60 and at -1e-300.Simulator.runfrom the same state, at rtol and atol of 1e-8 and of 1e-13, runs to t = 1e6 on every Jacobian and moves nothing, by 8e-38 at most. The time course retries a non-finite f at the nonnegative-clamped state (GH Extract the two forward-sensitivity jump handlers out of CvodeSimulator::run (follow-up to #109) #135). The steady-state march has never had that retry.So the run test refused a steady state over a NaN that only its own integrator met.
The first version of this PR was wrong
It retried the run test on difference quotients. That passed on macOS, where the FD run happened to step around the NaN. It failed on Linux CI, where from the exact-0 state the FD run also fails at t = 0. I reproduced the Linux failure in an x86-64 container, and on macOS by starting the solve from the Linux state's bits. That retry is removed.
The fix
src/steady_state.cpp. The run test now integrates as the time course does. Where f is NaN, with no infinity, it is taken again at the nonnegative-clamped state, and only then. A model whose f is finite runs as before, to the bit.An infinite f is not retried. Since this branch was opened, main's time course spares a slot it reads at a pole (
spare_at_pole). There, BIOMD0000000338's XIIa and K were clamped into a wrong answer, -1. A NaN is still clamped at any depth. So the run test takes only the NaN case, and a run test that meets an infinity is refused, as on main.cvode_ss_rhsand the colored difference quotient (KLU) both go throughss_eval_rhs, so the quotient is formed from the same f asfy. The dense quotient callscvode_ss_rhs, so it gets the retry with no extra change.The closed-form Jacobians, dense and sparse, get the same NaN-only retry in the run test. This is the Jacobian half of GH #135. CVODE evaluates the Jacobian at its prediction. At the Linux state, AKT(s~P) is exactly 0 with f = -1.1e-39, so the first prediction puts it near -1e-43, and d/dx x^3.5 = 3.5·x^2.5 is NaN there.
include/bngsim/nonneg_clamp.hpp(new). This holds the rule for which slots may be clamped (#706: not fixed, reported, not declared negative, not a rate-rule target). It moved verbatim out ofcvode_simulator.cpp, whosenonneg_clamp_masknow calls it. Main'sclamp_state_nonneg, with itsspareslots, is untouched. The time course is unchanged.What stays unclamped, and why:
The march. I tried clamping the whole march first (mutant M1 below). It accepts states with AKT(s~P) a hair below 0, where the unclamped residual is NaN. Four tests fail, including two existing march tests.
The take-on run (
ss_continued_run_once(..., &end)), whose end state the solve adopts. That run belongs to the march, so it stays as the march is. Clamping it too (M5) changes no test and no corpus result. The exclusion is conservative, not forced.Note on the Jacobian retry. The previous push left it out, because no macOS test needed it (M3 survived). The Linux shards then failed
test_a_compiled_march_to_a_fractional_power_at_zero_is_not_refused: from the Linux state, the closed-form run test still failed at t = 0. The three pinned-state tests passed there. I could not reach that run test from Python on macOS: from the same state, the march's own first step fails and the polish moves the state. So I used a temporary build hook that runs the run test from the solve's initial state, and measured it from the Linux bits:fd, interpretedfd, compiledThe hook is not in the PR. The compiled test is the regression test for this half, and it fails on x86-64 Linux without it.
Tests (
test_steady_state_scaled_criterion.py)test_a_compiled_march_to_a_fractional_power_at_zero_is_not_refused: glioblastoma withcodegen=Trueconverges, with run measure ≤ 1. The source iscodegen,solver_jacobian_retriedis False, and the concentrations match the interpreted solve's at the march's tolerances, 1e-8.test_a_run_test_from_a_fractional_power_at_zero_is_taken_to_its_time[auto | fd | fd colored]: a solve started from the Linux state's exact bits. It converges with run measure ≤ 1, at that state to 1e-8.fd coloredforces KLU and asserts it. This test needs no Linux: on main it fails on macOS too.On main's core: three of the four fail, each with
converged=False.[auto]passes there; it is a check, not a regression test.The previous control,
test_an_explicit_closed_form_is_not_called_off_in_the_run_test, asserted thatjacobian="analytical"is still refused. The cause was never the Jacobian, so that solve now converges too, and the test is removed.Mutations (C++ rebuilt for each; all of
test_steady_state_scaled_criterion.py)fdtests, plus..._returns_the_state_it_passed[integration]and..._still_retried_on_difference_quotientsss_eval_rhs[fd]and[fd colored][fd colored]test_clamp_far_below_zero.py. No test or corpus model meets an infinity in the run test; NaN-only follows main's time course (above)Measured
Corpus. Every
.netmodel in the repo (177, fromtests/data,benchmarksandparity_checks),steady_state()interpreted and compiled. Before the merge of main, against main's core:fdM5's sweep is identical to this PR's on all 354. On the final code (main merged, Jacobian retry in), against the same tree with the run test's switch off (M4, main's steady-state behaviour): default Jacobian, 353 identical, and glioblastoma compiled goes refused → converged at 2.15e-39.
fd, 353 identical, and glioblastoma interpreted has the same state and residual, with run measure 0.022 → 0.57, as above.With #1350. Under
BNGSIM_CODEGEN_THRESHOLD=1:test_a_march_whose_integrator_fails_returns_the_state_it_passed[integration], which tests: declare what a forced-JIT run cannot pass, so the run is a signal (#1217) #1350 marks as a strict xfail citing steady_state(method='integration') on the compiled RHS and Jacobian refuses a state at residual 2e-39 and does not retry on difference quotients #1357, now passes. Whichever PR merges second drops that mark.test_a_march_whose_integrator_fails_is_still_retried_on_difference_quotients(MODEL1712050008) fails identically on main and here:converged=True, residual 1.05e-15,solver_jacobian_retried=False. It pins the arm64 interpreter's route, not this bug, and tests: declare what a forced-JIT run cannot pass, so the run is a signal (#1217) #1350 moves it to itsthreshold_forcedskips.Full suite on the final code (main c0877fa merged): 16,419 passed, 0 failed, 138 skipped.
Linux. I reproduced the original failure in an x86-64 Linux container. Docker would not start for the later versions, so this PR's Linux CI shards are the x86-64 check. On the previous push they passed the three pinned-state cases and failed only the compiled one, for the Jacobian reason above.