Repository navigation
Place events on the time elapsed since the call started (#110) - #119
Merged
Merged
Conversation
Since #98 a run's seed state sits at ts.t_start on the model's clock, and run_ssa placed every event at current_time + tau in doubles. Once the total propensity times |t| nears 1e15, tau is below the spacing of the doubles near t and is rounded away or up to a whole spacing: the run fires too slowly or not at all, with no error. The tolerance for "this sample is at the start", 1e-12 * |t_end|, is 10 time units from t_start = 1e13, so a sample at t_start + 2 recorded the seed state. A(s~0) -> A(s~1) at k = 0.5 from 1e5 copies, mean of 10 seeds at t0 + 2 against 1e5/e = 36788: t0 main this 0 36820.8 z = +0.7 36820.8 1e9 36820.5 z = +0.7 36820.8 1e12 22824.8 z = -290 36820.8 1e13 100000 (seed state) 36820.8 1e15 100000 (seed state) 36820.8 The same decay at k + a*(time() - T0) from 1e13 does not finish on main in 20 minutes; a stack sample finds it inside next_clocked_event_time. It takes a fraction of a second here. run_ssa now keeps the clock as an origin, current_time where the call starts, plus the time elapsed since. Events, sample positions and the clock-driven hazard's panels go on the elapsed part; current_time, which time(), a TFUN's time counter, the sample labels and save_state read, is origin + elapsed. The origin is set again at every call (run, simulate, step_to), so resolution depends on how long a call is, not where it sits, and nothing new goes into the state file: a continuation in process and one from a saved state start from the same current_time. A grid point sits at start + i*dt on the elapsed clock, so from 1e16, where doubles are 2 apart and the labels t0 + 0.5*i round together, the states are still the run from 0's. t_eps scales with the call's span. A call that runs to the end leaves current_time at ts.t_end itself, not origin + (t_end - origin), which for 0.7 and 2.9 is 2.9000000000000004. Only the clock value a rate law reads is rounded, which BNG's network simulate has too. Adjacent fixes, each found while checking this one: - A session of a model whose rates do not read the clock kept the time of its last event once nothing could fire. The absorbing branch recorded the remaining samples and stopped without moving the clock to t_end, so the next simulate() from t_end was refused for starting after the session's time, and a session from an empty pool stayed at its start. The clock now goes to t_end, as it does when a draw outlasts the call. A parameter_scan carry-over chain's point after one that decays out now starts at t_end, not at the last event. - An infinite t_end was accepted and went wrong. run({0, inf, 2}) labelled its rows NaN, inf, inf with every one at the end state; with no grid, both rows held the seed state; a model that never stops firing never returned. With the fix above, step_to(inf) on a pool that decays out would leave current_time() at inf, which load_state cannot read back. Engine::run, which every run, simulate, step_to, ensemble replicate and scan point goes through, now refuses a t_end that is not finite, as initialize() already refuses such a t_start. simulate()'s start check, |t_start - session time| > tol, was false for a NaN t_start, which then ran with its first row labelled NaN; it is written so NaN fails it. - simulate(TimeSpec) with sample_times checked only the last sample against the session's time, so a t_end before it ran nothing and labelled the session's state with the later sample times. That includes t_end left at its default 0, which is how session_sample_times_test's mid-protocol case called it: its rows at 6.5, 8 and 10 were the state at 5, and the test checked only the labels. The refusal matches simulate(t_start, t_end, n). The test now sets t_end and checks the segment against a uniform second segment at the instants they share, bit for bit. Blast radius, rm_driver --print-functions against main over all 262 XMLs under tests/, seeds 1 and 2: - one-shot from t = 0: 262/262 byte-identical; - one-shot from --t-start 5 over the same span: 262/262 byte-identical, rates that read the clock included; - split at half the run (--save-state, then --load-state --t-start): 251/262 byte-identical. The 11 that differ reach an absorbing state before the split: main saves the last event's time and refuses its own continuation, and this branch continues. ctest 67/67 (late_start_test, clock_origin_test and session_sample_times_test also under asan), harness/basicmodels.py 29/29. late_start_test checks the following: - seed for seed, that a run over [t0, t0 + 2] from 1e9, 1e12, 1e13 and 1e15 is the run over [0, 2], on a grid and on listed sample times, on the plain path and on the clocked one with a flat rate; - that a grid from 1e16 is the run from 0 too; - that a late session advanced by two simulate() calls, or by step_to and simulate(), is the session from 0; - that parameter_scan's fresh points and carry-over chain from a late start are the ones from 0; - that a session parked at 1e13, then given molecules, runs as one at 0; - that a late session continued in process and from its saved state agree; - that a ramp, a step and a pulse reading time() - T0 from 1e13 match their closed forms; - that clock() reads each label; - that a TFUN time counter reads the model's clock in a session's second call; - that a call ends at exactly t_end; - that a session, and a carry-over chain, continue past an absorbing state; - and the refusals. Against main an earlier version of it failed 10 checks, and the late ramp does not finish; the test has a 300 s timeout. clock_origin_test's flat run over [5, 7] now matches the one over [0, 2] exactly, where it allowed a difference of 1. Mutations: 20 of 22 killed. The two survivors cannot change output: - grid points at i*dt rather than at the segment start's offset, which differ only when a session's t_start is within simulate()'s tolerance of its clock but not equal to it; - the t_end sample of a call with no grid, which records the final state either way.
The ascending-order check compared neighbours with <, which is false
for NaN, so a NaN passed it. The recorder's cursor then stopped on it,
since no clock reading is >= NaN, and every sample after it was recorded
at the final state. run() with sample_times {0, NaN, 1, 2, 4} on
late_start_model at seed 1 gave A0 = 1341 at every row from the NaN on,
where 1 and 2 should read about 6065 and 3679. Present on main; it is
the same class as the NaN segment start the previous commit refuses, and
the PR's description of that refusal would otherwise suggest the
non-finite holes were closed.
run_ssa now refuses a NaN anywhere in the list, before any event, so a
session is left where it was. Finite lists, and every rm_driver run,
which never passes sample_times, take the same path as before.
late_start_test checks that run() and simulate(TimeSpec) both refuse
such a list and that the session does not move. The mutant without the
guard fails it. Against main, the test now fails 24 checks, with the
late ramp, step and pulse skipped because they never finish there.
ctest 67/67.
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 #110.
Since #98 a run's seed state sits at ts.t_start on the model's clock, and
run_ssa placed every event at current_time + tau in doubles. Once the
total propensity times |t| nears 1e15, tau is below the spacing of the
doubles near t and is rounded away or up to a whole spacing: the run fires
too slowly or not at all, with no error. The tolerance for "this sample is
at the start", 1e-12 * |t_end|, is 10 time units from t_start = 1e13, so a
sample at t_start + 2 recorded the seed state. A(s
0) -> A(s1) atk = 0.5 from 1e5 copies, mean of 10 seeds at t0 + 2 against 1e5/e = 36788:
The same decay at k + a*(time() - T0) from 1e13 does not finish on main in
20 minutes; a stack sample finds it inside next_clocked_event_time. It
takes a fraction of a second here.
run_ssa now keeps the clock as an origin, current_time where the call
starts, plus the time elapsed since. Events, sample positions and the
clock-driven hazard's panels go on the elapsed part; current_time, which
time(), a TFUN's time counter, the sample labels and save_state read, is
origin + elapsed. The origin is set again at every call (run, simulate,
step_to), so resolution depends on how long a call is, not where it sits,
and nothing new goes into the state file: a continuation in process and
one from a saved state start from the same current_time. A grid point
sits at start + idt on the elapsed clock, so from 1e16, where doubles are
2 apart and the labels t0 + 0.5i round together, the states are still the
run from 0's. t_eps scales with the call's span. A call that runs to the
end leaves current_time at ts.t_end itself, not origin + (t_end - origin),
which for 0.7 and 2.9 is 2.9000000000000004. Only the clock value a rate
law reads is rounded, which BNG's network simulate has too.
Adjacent fixes, each found while checking this one:
A session of a model whose rates do not read the clock kept the time of
its last event once nothing could fire. The absorbing branch recorded
the remaining samples and stopped without moving the clock to t_end, so
the next simulate() from t_end was refused for starting after the
session's time, and a session from an empty pool stayed at its start.
The clock now goes to t_end, as it does when a draw outlasts the call.
A parameter_scan carry-over chain's point after one that decays out now
starts at t_end, not at the last event.
An infinite t_end was accepted and went wrong. run({0, inf, 2})
labelled its rows NaN, inf, inf with every one at the end state; with
no grid, both rows held the seed state; a model that never stops firing
never returned. With the fix above, step_to(inf) on a pool that decays
out would leave current_time() at inf, which load_state cannot read
back. Engine::run, which every run, simulate, step_to, ensemble
replicate and scan point goes through, now refuses a t_end that is not
finite, as initialize() already refuses such a t_start. simulate()'s
start check, |t_start - session time| > tol, was false for a NaN
t_start, which then ran with its first row labelled NaN; it is written
so NaN fails it. A NaN anywhere in sample_times is refused too (second
commit): it passed the ascending-order check and stalled the sample
cursor, so with {0, NaN, 1, 2, 4} the rows at 1 and 2 held the state
at 4.
simulate(TimeSpec) with sample_times checked only the last sample
against the session's time, so a t_end before it ran nothing and
labelled the session's state with the later sample times. That
includes t_end left at its default 0, which is how
session_sample_times_test's mid-protocol case called it: its rows at
6.5, 8 and 10 were the state at 5, and the test checked only the
labels. The refusal matches simulate(t_start, t_end, n). The test now
sets t_end and checks the segment against a uniform second segment at
the instants they share, bit for bit.
Blast radius, rm_driver --print-functions against main over all 262 XMLs
under tests/, seeds 1 and 2:
rates that read the clock included;
251/262 byte-identical. The 11 that differ reach an absorbing state
before the split: main saves the last event's time and refuses its own
continuation, and this branch continues.
ctest 67/67 (late_start_test, clock_origin_test and session_sample_times_test
also under asan), harness/basicmodels.py 29/29.
late_start_test checks the following:
1e15 is the run over [0, 2], on a grid and on listed sample times, on
the plain path and on the clocked one with a flat rate;
and simulate(), is the session from 0;
start are the ones from 0;
agree;
their closed forms;
call;
state;
Against main it fails 24 checks, and the late ramp, step and pulse do not
finish there, so the count is taken with those three skipped; the test has
a 300 s timeout. clock_origin_test's flat run over
[5, 7] now matches the one over [0, 2] exactly, where it allowed a
difference of 1.
Mutations: 21 of 23 killed. The two survivors cannot change output:
differ only when a session's t_start is within simulate()'s
tolerance of its clock but not equal to it;
state either way.