Repository navigation
Conversation
…anl#1286) A Gaussian or sech^2 bump of a clock underflows to exactly 0 away from it, so CVODE's error estimate is 0 and a step grows over the whole bump: Y came back 0. smooth_pulse_stops finds exp of a quadratic, and cosh/sech/tanh of an affine argument, in one clock (time() or a constant-rate counter at any rate) in the value of each functional rate law, and adds a crossing stop at the centre. Bumps inside conditions are left to the condition scan. A hold-log test is split: the original runs without the bump stops, and a strict xfail twin records a bug on main where a crossing stop keeps the hold's error log muted.
This branch has not been deployed
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 #1286.
What was wrong
A rate law shaped as a smooth narrow bump of a clock is stepped over entirely, with no error and no warning. Examples are a Gaussian
exp(-((T - c)/s)^2)and a sech²1/cosh((T - c)/s)^2. Away from the bump the rate underflows to exactly 0 (exp(-10^4)is 0 in doubles). So the right-hand side is exactly what it would be with no bump, CVODE's error estimate is 0, and its step grows until one step spans the bump. Corners and conditions get stops or roots (#714, #1068, #1127), but a smooth bump has neither.The issue's model: X → Y at a Gaussian of area 0.5 in a counter T fed at rate 2, centred at T = 50 (t = 25), so Y(30) = 10(1 − e^(−1/4)) = 2.211992. On main c0877fa, Y(30) = 0 at w = 0.01 and w = 1, at rtol 1e-6 and 1e-12, at 301 output points, and in a sensitivity run. The same holds for sech² and for
time()in place of the counter.The fix
New
python/bngsim/_smooth_pulses.py.smooth_pulse_stops(core, t_start, t_end)finds the bumps and returns a stop at each one's centre. A step that would have spanned the bump ends on its peak instead, where the rate is largest, so the error test sees it._exprtk_to_sympy, with the run's parameter values put in:exp(q), whereqis a quadratic in one clock with a negative leading coefficient (a Gaussian at any scale or offset); its centre is −b/2a;cosh(u),sech(u)ortanh(u), whereuis affine in one clock. That covers sech² spelled1/cosh(u)^2or1 - tanh(u)^2(tanh saturates to exactly 1, so the second is also 0 away from the bump). A plain tanh step gets a stop it does not need, which costs one restart.time(), or any observable all of whose species change at one constant rate. That is found by two probes of the right-hand side at shifted states and times, asunit_rate_clock_candidatesdoes, but at any rate: the issue's counter is fed at k_clock = 2, which the unit-rate machinery does not take for a clock. A counter's centre is converted to a time from its value att_startand its rate.Simulator._apply_crossing_stopsonly. The stops join the condition, periodic and table-function stops inmerge_crossing_stops. They are not added totime_discontinuity_conditions(), so the sensitivity switch-time machinery, the state-switch roots and the other consumers of that list never see a bump as a switch.if(exp(...) > 0.5, ...), a threshold written withtanh) is a crossing, which the condition scan places or declines (A narrow if() window is still stepped over when written as a number, as one non-monotone comparison, or reached through steady_state() (after #897) #904, A floor() or max() pulse on a counter that is not unit-rate gets no stop and no root, so it is stepped over: Y = 0 for 3.9347, with no error (the if() spelling is rooted) #1068). A stop from here would override what that scan decided. Two existing tests caught that in a first version:test_clock_condition_scan.py::test_a_text_the_evaluator_cannot_be_asked_about_is_declinedand the moving-threshold window test intest_net_state_switch_stepping.py. Both pass unchanged now.exp,cosh,sechortanhis answered from the raw texts, without building the inlined context (0.01 ms measured). The answer is cached on everything it reads: function texts, parameter values, state and window. A repeat costs 0.03 ms.One stop, at the peak. I first placed stops at the centre and four widths either side. Against a
max_stepreference I measured a Gaussian of widths 10, 3, 0.5 and 0.005, two bumps in one run, and a run that starts on a bump's rise, at rtol 1e-6 and 1e-12. The centre stop alone was within the tolerance in every case, as the three were, and is two restarts fewer per bump.Tests (
test_smooth_pulse_stops.py, 20)test_a_bump_in_a_counter_is_integrated[Gaussian, sech² (cosh), sech² (1-tanh²)] × [w 0.01, 1] × [rtol 1e-6, 1e-12]: Y(30) = 2.211992test_a_bump_in_literal_time_is_integratedtest_dense_output_does_not_step_over_it_either(301 points)test_a_sensitivity_run_integrates_the_bump: Y and dY/dkx = 2.5·e^(−1/4) to 1e-5test_the_stop_is_at_the_centre(t = 25 on the rate-2 counter);test_a_stop_outside_the_run_is_not_placedA test this changes
test_net_state_switch_stepping.py::test_a_released_hold_logs_what_main_logsfailed with the bump stops in. That test checks that a state-switch hold gives CVODE's error log back once it ends, by counting the batch-end messages logged after the release. The count fell to 0 with a bump stop, and also on main with an ordinary condition stop.I first took this for a bug and filed it as #1389. Traced, it is not one:
The test now runs with the bump stops patched out (
pulse_stops=False), the run it was written for, and its comment says why. #1389 can be closed.Mutations
tanhnot recognized1-tanh²casescoshnot recognizedcoshsech² cases_value_callswalks every node)Measured
.netmodel in the repo and the 1,323 BioModels SBML files, 1,468 of which load, over t = 0–100. No model gets a bump stop, so no corpus answer moves. The change is reached only by a model that writes such a bump.pre-commit run --all-files(new files staged) andsphinx -Wwithdocs/requirements-ci.txtare clean.Not in this PR
exp(-u)/(1+exp(-u))^2, for example, has anexpof an affine argument, which is not taken for a bump.max_stepunder the bump's width remains the workaround there.1/cosh(u)^2is ∞/∞ far from the centre). That one fails loudly. The issue comment's1 - tanh(u)^2spelling avoids it, and this PR integrates that spelling.